diff --git a/AUTHORS.rst b/AUTHORS.rst
index 5be3cfd6..18559763 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -20,10 +20,7 @@ answer newbie questions, and generally made taiga that much better:
- Andrea Stagi
- Andrés Moya
- Andrey Alekseenko
-<<<<<<< HEAD
-=======
- Brett Profitt
->>>>>>> master
- Bruno Clermont
- Chris Wilson
- David Burke
@@ -32,7 +29,10 @@ answer newbie questions, and generally made taiga that much better:
- Joe Letts
- Julien Palard
- luyikei
+- Michael Jurke
- Motius GmbH
+- Riccardo Coccioli
- Ricky Posner
+- Stefan Auditor
- Yamila Moreno
- Yaser Alraddadi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a337874..108833e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,51 @@
# Changelog #
+
+## 3.0.0 Stellaria Borealis (2016-10-02)
+
+### Features
+- Add Epics.
+- Include created, modified and finished dates for tasks in CSV reports.
+- Add gravatar url to Users API endpoint.
+- ProjectTemplates now are sorted by the attribute 'order'.
+- Create enpty wiki pages (if not exist) when a new link is created.
+- Diff messages in history entries now show only the relevant changes (with some context).
+- User stories and tasks listing API call support extra params to include more data (tasks and attachemnts and attachments, respectively)
+- Comments:
+ - Now comment owners and project admins can edit existing comments with the history Entry endpoint.
+ - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose.
+- Tags:
+ - New API endpoints over projects to create, rename, edit, delete and mix tags.
+ - Tag color assignation is not automatic.
+ - Select a color (or not) to a tag when add it to stories, issues and tasks.
+- Improve search system over stories, tasks and issues:
+ - Search into tags too. (thanks to [Riccardo Cocciol](https://github.com/volans-))
+ - Weights are applied: (subject = ref > tags > description).
+- Import/Export:
+ - Gzip export/import support.
+ - Export performance improvements.
+- Add filter by email domain registration and invitation by setting.
+- Third party integrations:
+ - Included gogs as builtin integration.
+ - Improve messages generated on webhooks input.
+ - Add mentions support in commit messages.
+ - Cleanup hooks code.
+ - Rework webhook signature header to align with larger implementations and defined [standards](https://superfeedr-misc.s3.amazonaws.com/pubsubhubbub-core-0.4.html\#authednotify). (thanks to [Stefan Auditor](https://github.com/sanduhrs))
+- Add created-, modified-, finished- and finish_date queryset filters
+ - Support exact match, gt, gte, lt, lte
+ - added issues, tasks and userstories accordingly
+- i18n:
+ - Add norwegian Bokmal (nb) translation.
+
+### Misc
+- [API] Improve performance of some calls over list.
+- Lots of small and not so small bugfixes.
+
+
## 2.1.0 Ursus Americanus (2016-05-03)
### Features
-- Add sprint name and slug on search results for user stories ((thanks to [@everblut](https://github.com/everblut)))
+- Add sprint name and slug on search results for user stories (thanks to [@everblut](https://github.com/everblut))
- [API] projects resource: Random order if `discover_mode=true` and `is_featured=true`.
- Webhooks: Improve webhook data:
- add permalinks
diff --git a/requirements.txt b/requirements.txt
index 3050180d..47ad6c46 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ six==1.10.0
amqp==1.4.9
djmail==0.12.0.post1
django-pgjson==0.3.1
-djorm-pgarray==1.2
+djorm-pgarray==1.2 # Use until Taiga 2.1. Keep compatibility with old migrations
django-jinja==2.1.2
jinja2==2.8
pygments==2.0.2
@@ -28,9 +28,10 @@ raven==5.10.2
bleach==1.4.3
django-ipware==1.1.3
premailer==2.9.7
-cssutils==1.0.1 # Compatible with python 3.5
+cssutils==1.0.1 # Compatible with python 3.5
lxml==3.5.0
git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea
pyjwkest==1.1.5
python-dateutil==2.4.2
netaddr==0.7.18
+serpy==0.1.1
diff --git a/scripts/generate_fixtures_initial_project_templates.sh b/scripts/generate_fixtures_initial_project_templates.sh
new file mode 100755
index 00000000..d0201489
--- /dev/null
+++ b/scripts/generate_fixtures_initial_project_templates.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+python ./manage.py dumpdata --format json \
+ --indent 4 \
+ --output './taiga/projects/fixtures/initial_project_templates.json' \
+ 'projects.ProjectTemplate'
diff --git a/settings/common.py b/settings/common.py
index f227d97c..87d0d904 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -124,7 +124,7 @@ LANGUAGES = [
#("mn", "Монгол"), # Mongolian
#("mr", "मराठी"), # Marathi
#("my", "မြန်မာ"), # Burmese
- #("nb", "Norsk (bokmål)"), # Norwegian Bokmal
+ ("nb", "Norsk (bokmål)"), # Norwegian Bokmal
#("ne", "नेपाली"), # Nepali
("nl", "Nederlands"), # Dutch
#("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk
@@ -300,6 +300,7 @@ INSTALLED_APPS = [
"taiga.projects.likes",
"taiga.projects.votes",
"taiga.projects.milestones",
+ "taiga.projects.epics",
"taiga.projects.userstories",
"taiga.projects.tasks",
"taiga.projects.issues",
@@ -313,6 +314,7 @@ INSTALLED_APPS = [
"taiga.hooks.github",
"taiga.hooks.gitlab",
"taiga.hooks.bitbucket",
+ "taiga.hooks.gogs",
"taiga.webhooks",
"djmail",
@@ -436,11 +438,14 @@ APP_EXTRA_EXPOSE_HEADERS = [
"taiga-info-total-opened-milestones",
"taiga-info-total-closed-milestones",
"taiga-info-project-memberships",
- "taiga-info-project-is-private"
+ "taiga-info-project-is-private",
+ "taiga-info-order-updated"
]
DEFAULT_PROJECT_TEMPLATE = "scrum"
PUBLIC_REGISTER_ENABLED = False
+# None or [] values in USER_EMAIL_ALLOWED_DOMAINS means allow any domain
+USER_EMAIL_ALLOWED_DOMAINS = None
SEARCHES_MAX_RESULTS = 150
@@ -477,10 +482,6 @@ THUMBNAIL_ALIASES = {
},
}
-# GRAVATAR_DEFAULT_AVATAR = "img/user-noimage.png"
-GRAVATAR_DEFAULT_AVATAR = ""
-GRAVATAR_AVATAR_SIZE = THN_AVATAR_SIZE
-
TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234",
"#73d216", "#4e9a06", "#d3d7cf", "#fcaf3e",
"#f57900", "#ce5c00", "#729fcf", "#3465a4",
@@ -508,6 +509,7 @@ PROJECT_MODULES_CONFIGURATORS = {
"github": "taiga.hooks.github.services.get_or_generate_config",
"gitlab": "taiga.hooks.gitlab.services.get_or_generate_config",
"bitbucket": "taiga.hooks.bitbucket.services.get_or_generate_config",
+ "gogs": "taiga.hooks.gogs.services.get_or_generate_config",
}
BITBUCKET_VALID_ORIGIN_IPS = ["131.103.20.165", "131.103.20.166", "104.192.143.192/28", "104.192.143.208/28"]
diff --git a/settings/local.py.example b/settings/local.py.example
index 4ae5a8ab..7defff37 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -18,6 +18,10 @@
from .development import *
+#########################################
+## GENERIC
+#########################################
+
#DEBUG = False
#ADMINS = (
@@ -54,6 +58,25 @@ DATABASES = {
#STATIC_ROOT = '/home/taiga/static'
+#########################################
+## THROTTLING
+#########################################
+
+#REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
+# "anon": "20/min",
+# "user": "200/min",
+# "import-mode": "20/sec",
+# "import-dump-mode": "1/minute"
+#}
+
+
+#########################################
+## MAIL SYSTEM SETTINGS
+#########################################
+
+#DEFAULT_FROM_EMAIL = "john@doe.com"
+#CHANGE_NOTIFICATIONS_MIN_INTERVAL = 300 #seconds
+
# EMAIL SETTINGS EXAMPLE
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
#EMAIL_USE_TLS = False
@@ -61,7 +84,6 @@ DATABASES = {
#EMAIL_PORT = 25
#EMAIL_HOST_USER = 'user'
#EMAIL_HOST_PASSWORD = 'password'
-#DEFAULT_FROM_EMAIL = "john@doe.com"
# GMAIL SETTINGS EXAMPLE
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
@@ -71,13 +93,22 @@ DATABASES = {
#EMAIL_HOST_USER = 'youremail@gmail.com'
#EMAIL_HOST_PASSWORD = 'yourpassword'
-# THROTTLING
-#REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
-# "anon": "20/min",
-# "user": "200/min",
-# "import-mode": "20/sec",
-# "import-dump-mode": "1/minute"
-#}
+
+#########################################
+## REGISTRATION
+#########################################
+
+#PUBLIC_REGISTER_ENABLED = True
+
+# LIMIT ALLOWED DOMAINS FOR REGISTER AND INVITE
+# None or [] values in USER_EMAIL_ALLOWED_DOMAINS means allow any domain
+#USER_EMAIL_ALLOWED_DOMAINS = None
+
+# PUCLIC OR PRIVATE NUMBER OF PROJECT PER USER
+#MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit
+#MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit
+#MAX_MEMBERSHIPS_PRIVATE_PROJECTS = None # None == no limit
+#MAX_MEMBERSHIPS_PUBLIC_PROJECTS = None # None == no limit
# GITHUB SETTINGS
#GITHUB_URL = "https://github.com/"
@@ -85,20 +116,37 @@ DATABASES = {
#GITHUB_API_CLIENT_ID = "yourgithubclientid"
#GITHUB_API_CLIENT_SECRET = "yourgithubclientsecret"
-# FEEDBACK MODULE (See config in taiga-front too)
-#FEEDBACK_ENABLED = True
-#FEEDBACK_EMAIL = "support@taiga.io"
-# STATS MODULE
-#STATS_ENABLED = False
-#FRONT_SITEMAP_CACHE_TIMEOUT = 60*60 # In second
+#########################################
+## SITEMAP
+#########################################
-# SITEMAP
# If is True /front/sitemap.xml show a valid sitemap of taiga-front client
#FRONT_SITEMAP_ENABLED = False
#FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second
-# CELERY
+
+#########################################
+## FEEDBACK
+#########################################
+
+# Note: See config in taiga-front too
+#FEEDBACK_ENABLED = True
+#FEEDBACK_EMAIL = "support@taiga.io"
+
+
+#########################################
+## STATS
+#########################################
+
+#STATS_ENABLED = False
+#FRONT_SITEMAP_CACHE_TIMEOUT = 60*60 # In second
+
+
+#########################################
+## CELERY
+#########################################
+
#from .celery import *
#CELERY_ENABLED = True
#
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..2bd4593c
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,11 @@
+[flake8]
+ignore = E41,E266
+max-line-length = 120
+exclude =
+ .git,
+ *__pycache__*,
+ *tests*,
+ *scripts*,
+ *migrations*,
+ *management*
+max-complexity = 10
diff --git a/taiga/auth/api.py b/taiga/auth/api.py
index 5d14d18f..df077b52 100644
--- a/taiga/auth/api.py
+++ b/taiga/auth/api.py
@@ -22,15 +22,16 @@ from enum import Enum
from django.utils.translation import ugettext as _
from django.conf import settings
+from taiga.base.api import validators
from taiga.base.api import serializers
from taiga.base.api import viewsets
from taiga.base.decorators import list_route
from taiga.base import exceptions as exc
from taiga.base import response
-from .serializers import PublicRegisterSerializer
-from .serializers import PrivateRegisterForExistingUserSerializer
-from .serializers import PrivateRegisterForNewUserSerializer
+from .validators import PublicRegisterValidator
+from .validators import PrivateRegisterForExistingUserValidator
+from .validators import PrivateRegisterForNewUserValidator
from .services import private_register_for_existing_user
from .services import private_register_for_new_user
@@ -44,7 +45,7 @@ from .permissions import AuthPermission
def _parse_data(data:dict, *, cls):
"""
Generic function for parse user data using
- specified serializer on `cls` keyword parameter.
+ specified validator on `cls` keyword parameter.
Raises: RequestValidationError exception if
some errors found when data is validated.
@@ -52,21 +53,21 @@ def _parse_data(data:dict, *, cls):
Returns the parsed data.
"""
- serializer = cls(data=data)
- if not serializer.is_valid():
- raise exc.RequestValidationError(serializer.errors)
- return serializer.data
+ validator = cls(data=data)
+ if not validator.is_valid():
+ raise exc.RequestValidationError(validator.errors)
+ return validator.data
# Parse public register data
-parse_public_register_data = partial(_parse_data, cls=PublicRegisterSerializer)
+parse_public_register_data = partial(_parse_data, cls=PublicRegisterValidator)
# Parse private register data for existing user
parse_private_register_for_existing_user_data = \
- partial(_parse_data, cls=PrivateRegisterForExistingUserSerializer)
+ partial(_parse_data, cls=PrivateRegisterForExistingUserValidator)
# Parse private register data for new user
parse_private_register_for_new_user_data = \
- partial(_parse_data, cls=PrivateRegisterForNewUserSerializer)
+ partial(_parse_data, cls=PrivateRegisterForNewUserValidator)
class RegisterTypeEnum(Enum):
@@ -81,10 +82,10 @@ def parse_register_type(userdata:dict) -> str:
"""
# Create adhoc inner serializer for avoid parse
# manually the user data.
- class _serializer(serializers.Serializer):
+ class _validator(validators.Validator):
existing = serializers.BooleanField()
- instance = _serializer(data=userdata)
+ instance = _validator(data=userdata)
if not instance.is_valid():
raise exc.RequestValidationError(instance.errors)
diff --git a/taiga/auth/serializers.py b/taiga/auth/validators.py
similarity index 72%
rename from taiga/auth/serializers.py
rename to taiga/auth/validators.py
index 8e8df4e2..a18dc4bc 100644
--- a/taiga/auth/serializers.py
+++ b/taiga/auth/validators.py
@@ -16,16 +16,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.core import validators
-from django.core.exceptions import ValidationError
+from django.core import validators as core_validators
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
import re
-class BaseRegisterSerializer(serializers.Serializer):
+class BaseRegisterValidator(validators.Validator):
full_name = serializers.CharField(max_length=256)
email = serializers.EmailField(max_length=255)
username = serializers.CharField(max_length=255)
@@ -33,25 +34,25 @@ class BaseRegisterSerializer(serializers.Serializer):
def validate_username(self, attrs, source):
value = attrs[source]
- validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid")
+ validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid")
try:
validator(value)
except ValidationError:
- raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
- "and /./-/_ characters'"))
+ raise ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
+ "and /./-/_ characters'"))
return attrs
-class PublicRegisterSerializer(BaseRegisterSerializer):
+class PublicRegisterValidator(BaseRegisterValidator):
pass
-class PrivateRegisterForNewUserSerializer(BaseRegisterSerializer):
+class PrivateRegisterForNewUserValidator(BaseRegisterValidator):
token = serializers.CharField(max_length=255, required=True)
-class PrivateRegisterForExistingUserSerializer(serializers.Serializer):
+class PrivateRegisterForExistingUserValidator(validators.Validator):
username = serializers.CharField(max_length=255)
password = serializers.CharField(min_length=4)
token = serializers.CharField(max_length=255, required=True)
diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py
index 7dfa2c0a..bab90160 100644
--- a/taiga/base/api/fields.py
+++ b/taiga/base/api/fields.py
@@ -50,7 +50,6 @@ They are very similar to Django's form fields.
from django import forms
from django.conf import settings
from django.core import validators
-from django.core.exceptions import ValidationError
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets
from django.http import QueryDict
@@ -66,6 +65,8 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
+from taiga.base.exceptions import ValidationError
+
from . import ISO_8601
from .settings import api_settings
@@ -611,6 +612,15 @@ class ChoiceField(WritableField):
return value
+def validate_user_email_allowed_domains(value):
+ validators.validate_email(value)
+
+ domain_name = value.split("@")[1]
+
+ if settings.USER_EMAIL_ALLOWED_DOMAINS and domain_name not in settings.USER_EMAIL_ALLOWED_DOMAINS:
+ raise ValidationError(_("You email domain is not allowed"))
+
+
class EmailField(CharField):
type_name = "EmailField"
type_label = "email"
@@ -619,7 +629,7 @@ class EmailField(CharField):
default_error_messages = {
"invalid": _("Enter a valid email address."),
}
- default_validators = [validators.validate_email]
+ default_validators = [validate_user_email_allowed_domains]
def from_native(self, value):
ret = super(EmailField, self).from_native(value)
diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py
index 158d712d..31823945 100644
--- a/taiga/base/api/generics.py
+++ b/taiga/base/api/generics.py
@@ -62,6 +62,7 @@ class GenericAPIView(pagination.PaginationMixin,
# or override `get_queryset()`/`get_serializer_class()`.
queryset = None
serializer_class = None
+ validator_class = None
# This shortcut may be used instead of setting either or both
# of the `queryset`/`serializer_class` attributes, although using
@@ -79,6 +80,7 @@ class GenericAPIView(pagination.PaginationMixin,
# The following attributes may be subject to change,
# and should be considered private API.
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
+ model_validator_class = api_settings.DEFAULT_MODEL_VALIDATOR_CLASS
######################################
# These are pending deprecation...
@@ -88,7 +90,7 @@ class GenericAPIView(pagination.PaginationMixin,
slug_field = 'slug'
allow_empty = True
- def get_serializer_context(self):
+ def get_extra_context(self):
"""
Extra context provided to the serializer class.
"""
@@ -101,14 +103,24 @@ class GenericAPIView(pagination.PaginationMixin,
def get_serializer(self, instance=None, data=None,
files=None, many=False, partial=False):
"""
- Return the serializer instance that should be used for validating and
- deserializing input, and for serializing output.
+ Return the serializer instance that should be used for deserializing
+ input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
- context = self.get_serializer_context()
+ context = self.get_extra_context()
return serializer_class(instance, data=data, files=files,
many=many, partial=partial, context=context)
+ def get_validator(self, instance=None, data=None,
+ files=None, many=False, partial=False):
+ """
+ Return the validator instance that should be used for validating the
+ input, and for serializing output.
+ """
+ validator_class = self.get_validator_class()
+ context = self.get_extra_context()
+ return validator_class(instance, data=data, files=files,
+ many=many, partial=partial, context=context)
def filter_queryset(self, queryset, filter_backends=None):
"""
@@ -119,7 +131,7 @@ class GenericAPIView(pagination.PaginationMixin,
method if you want to apply the configured filtering backend to the
default queryset.
"""
- #NOTE TAIGA: Added filter_backends to overwrite the default behavior.
+ # NOTE TAIGA: Added filter_backends to overwrite the default behavior.
backends = filter_backends or self.get_filter_backends()
for backend in backends:
@@ -160,6 +172,22 @@ class GenericAPIView(pagination.PaginationMixin,
model = self.model
return DefaultSerializer
+ def get_validator_class(self):
+ validator_class = self.validator_class
+ serializer_class = self.get_serializer_class()
+
+ # Situations where the validator is the rest framework serializer
+ if validator_class is None and serializer_class is not None:
+ return serializer_class
+
+ if validator_class is not None:
+ return validator_class
+
+ class DefaultValidator(self.model_validator_class):
+ class Meta:
+ model = self.model
+ return DefaultValidator
+
def get_queryset(self):
"""
Get the list of items for this view.
diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py
index 89af6984..b01d7cf2 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -44,12 +44,12 @@
import warnings
-from django.core.exceptions import ValidationError
from django.http import Http404
from django.db import transaction as tx
from django.utils.translation import ugettext as _
from taiga.base import response
+from taiga.base.exceptions import ValidationError
from .settings import api_settings
from .utils import get_object_or_404
@@ -57,6 +57,7 @@ from .utils import get_object_or_404
from .. import exceptions as exc
from ..decorators import model_pk_lock
+
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
Given a model instance, and an optional pk and slug field,
@@ -89,19 +90,21 @@ class CreateModelMixin:
Create a model instance.
"""
def create(self, request, *args, **kwargs):
- serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+ validator = self.get_validator(data=request.DATA, files=request.FILES)
- if serializer.is_valid():
- self.check_permissions(request, 'create', serializer.object)
+ if validator.is_valid():
+ self.check_permissions(request, 'create', validator.object)
- self.pre_save(serializer.object)
- self.pre_conditions_on_save(serializer.object)
- self.object = serializer.save(force_insert=True)
+ self.pre_save(validator.object)
+ self.pre_conditions_on_save(validator.object)
+ self.object = validator.save(force_insert=True)
self.post_save(self.object, created=True)
+ instance = self.get_queryset().get(id=self.object.id)
+ serializer = self.get_serializer(instance)
headers = self.get_success_headers(serializer.data)
return response.Created(serializer.data, headers=headers)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
def get_success_headers(self, data):
try:
@@ -171,28 +174,32 @@ class UpdateModelMixin:
if self.object is None:
raise Http404
- serializer = self.get_serializer(self.object, data=request.DATA,
- files=request.FILES, partial=partial)
+ validator = self.get_validator(self.object, data=request.DATA,
+ files=request.FILES, partial=partial)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
# Hooks
try:
- self.pre_save(serializer.object)
- self.pre_conditions_on_save(serializer.object)
+ self.pre_save(validator.object)
+ self.pre_conditions_on_save(validator.object)
except ValidationError as err:
# full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors.
return response.BadRequest(err.message_dict)
if self.object is None:
- self.object = serializer.save(force_insert=True)
+ self.object = validator.save(force_insert=True)
self.post_save(self.object, created=True)
+ instance = self.get_queryset().get(id=self.object.id)
+ serializer = self.get_serializer(instance)
return response.Created(serializer.data)
- self.object = serializer.save(force_update=True)
+ self.object = validator.save(force_update=True)
self.post_save(self.object, created=False)
+ instance = self.get_queryset().get(id=self.object.id)
+ serializer = self.get_serializer(instance)
return response.Ok(serializer.data)
def partial_update(self, request, *args, **kwargs):
@@ -204,14 +211,14 @@ class UpdateModelMixin:
Set any attributes on the object that are implicit in the request.
"""
# pk and/or slug attributes are implicit in the URL.
- lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
- lookup = self.kwargs.get(lookup_url_kwarg, None)
+ ##lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ ##lookup = self.kwargs.get(lookup_url_kwarg, None)
pk = self.kwargs.get(self.pk_url_kwarg, None)
slug = self.kwargs.get(self.slug_url_kwarg, None)
slug_field = slug and self.slug_field or None
- if lookup:
- setattr(obj, self.lookup_field, lookup)
+ ##if lookup:
+ ## setattr(obj, self.lookup_field, lookup)
if pk:
setattr(obj, 'pk', pk)
@@ -246,12 +253,33 @@ class DestroyModelMixin:
return response.NoContent()
+class NestedViewSetMixin(object):
+ def get_queryset(self):
+ return self._filter_queryset_by_parents_lookups(super().get_queryset())
+
+ def _filter_queryset_by_parents_lookups(self, queryset):
+ parents_query_dict = self._get_parents_query_dict()
+ if parents_query_dict:
+ return queryset.filter(**parents_query_dict)
+ else:
+ return queryset
+
+ def _get_parents_query_dict(self):
+ result = {}
+ for kwarg_name in self.kwargs:
+ query_value = self.kwargs.get(kwarg_name)
+ result[kwarg_name] = query_value
+ return result
+
+
+## TODO: Move blocked mixind out of the base module because is related to project
+
class BlockeableModelMixin:
def is_blocked(self, obj):
raise NotImplementedError("is_blocked must be overridden")
def pre_conditions_blocked(self, obj):
- #Raises permission exception
+ # Raises permission exception
if obj is not None and self.is_blocked(obj):
raise exc.Blocked(_("Blocked element"))
diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py
index b6f9ade4..b03d6c18 100644
--- a/taiga/base/api/permissions.py
+++ b/taiga/base/api/permissions.py
@@ -21,11 +21,12 @@ import abc
from functools import reduce
from taiga.base.utils import sequence as sq
-from taiga.permissions.service import user_has_perm, is_project_admin
+from taiga.permissions.services import user_has_perm, is_project_admin
from django.apps import apps
from django.utils.translation import ugettext as _
+
######################################################################
# Base permissiones definition
######################################################################
@@ -180,33 +181,6 @@ class HasProjectPerm(PermissionComponent):
return user_has_perm(request.user, self.project_perm, obj)
-class HasProjectParamAndPerm(PermissionComponent):
- def __init__(self, perm, *components):
- self.project_perm = perm
- super().__init__(*components)
-
- def check_permissions(self, request, view, obj=None):
- Project = apps.get_model('projects', 'Project')
- project_id = request.QUERY_PARAMS.get("project", None)
- try:
- project = Project.objects.get(pk=project_id)
- except Project.DoesNotExist:
- return False
- return user_has_perm(request.user, self.project_perm, project)
-
-
-class HasMandatoryParam(PermissionComponent):
- def __init__(self, param, *components):
- self.mandatory_param = param
- super().__init__(*components)
-
- def check_permissions(self, request, view, obj=None):
- param = request.GET.get(self.mandatory_param, None)
- if param:
- return True
- return False
-
-
class IsProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
return is_project_admin(request.user, obj)
@@ -214,6 +188,9 @@ class IsProjectAdmin(PermissionComponent):
class IsObjectOwner(PermissionComponent):
def check_permissions(self, request, view, obj=None):
+ if obj.owner is None:
+ return False
+
return obj.owner == request.user
diff --git a/taiga/base/api/relations.py b/taiga/base/api/relations.py
index 60ba9a6e..6fbb98f5 100644
--- a/taiga/base/api/relations.py
+++ b/taiga/base/api/relations.py
@@ -48,7 +48,7 @@ Serializer fields that deal with relationships.
These fields allow you to specify the style that should be used to represent
model relationships, including hyperlinks, primary keys, or slugs.
"""
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
@@ -59,6 +59,7 @@ from django.utils.translation import ugettext_lazy as _
from .fields import Field, WritableField, get_component, is_simple_callable
from .reverse import reverse
+from taiga.base.exceptions import ValidationError
import warnings
from urllib import parse as urlparse
diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py
index a9e5f139..2ee05db8 100644
--- a/taiga/base/api/serializers.py
+++ b/taiga/base/api/serializers.py
@@ -69,6 +69,7 @@ import copy
import datetime
import inspect
import types
+import serpy
# Note: We do the following so that users of the framework can use this style:
#
@@ -77,6 +78,8 @@ import types
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
+from taiga.base.exceptions import ValidationError
+
from .relations import *
from .fields import *
@@ -1220,3 +1223,27 @@ class HyperlinkedModelSerializer(ModelSerializer):
"model_name": model_meta.object_name.lower()
}
return self._default_view_name % format_kwargs
+
+
+class LightSerializer(serpy.Serializer):
+ def __init__(self, *args, **kwargs):
+ kwargs.pop("read_only", None)
+ kwargs.pop("partial", None)
+ kwargs.pop("files", None)
+ context = kwargs.pop("context", {})
+ view = kwargs.pop("view", {})
+ super().__init__(*args, **kwargs)
+ self.context = context
+ self.view = view
+
+
+class LightDictSerializer(serpy.DictSerializer):
+ def __init__(self, *args, **kwargs):
+ kwargs.pop("read_only", None)
+ kwargs.pop("partial", None)
+ kwargs.pop("files", None)
+ context = kwargs.pop("context", {})
+ view = kwargs.pop("view", {})
+ super().__init__(*args, **kwargs)
+ self.context = context
+ self.view = view
diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py
index 1a3d01ba..75d204c9 100644
--- a/taiga/base/api/settings.py
+++ b/taiga/base/api/settings.py
@@ -98,6 +98,8 @@ DEFAULTS = {
# Genric view behavior
"DEFAULT_MODEL_SERIALIZER_CLASS":
"taiga.base.api.serializers.ModelSerializer",
+ "DEFAULT_MODEL_VALIDATOR_CLASS":
+ "taiga.base.api.validators.ModelValidator",
"DEFAULT_FILTER_BACKENDS": (),
# Throttling
diff --git a/tests/unit/test_permissions.py b/taiga/base/api/validators.py
similarity index 78%
rename from tests/unit/test_permissions.py
rename to taiga/base/api/validators.py
index 5ef7a93d..3a8d6922 100644
--- a/tests/unit/test_permissions.py
+++ b/taiga/base/api/validators.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
@@ -15,12 +16,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.permissions import service
-from taiga.users.models import Role
+from . import serializers
-def test_role_has_perm():
- role = Role()
- role.permissions = ["test"]
- assert service.role_has_perm(role, "test")
- assert service.role_has_perm(role, "false") is False
+class Validator(serializers.Serializer):
+ pass
+
+
+class ModelValidator(serializers.ModelSerializer):
+ pass
diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py
index 95b09055..d37bfc50 100644
--- a/taiga/base/api/viewsets.py
+++ b/taiga/base/api/viewsets.py
@@ -134,6 +134,25 @@ class ViewSetMixin(object):
return super().check_permissions(request, action=action, obj=obj)
+class NestedViewSetMixin(object):
+ def get_queryset(self):
+ return self._filter_queryset_by_parents_lookups(super().get_queryset())
+
+ def _filter_queryset_by_parents_lookups(self, queryset):
+ parents_query_dict = self._get_parents_query_dict()
+ if parents_query_dict:
+ return queryset.filter(**parents_query_dict)
+ else:
+ return queryset
+
+ def _get_parents_query_dict(self):
+ result = {}
+ for kwarg_name in self.kwargs:
+ query_value = self.kwargs.get(kwarg_name)
+ result[kwarg_name] = query_value
+ return result
+
+
class ViewSet(ViewSetMixin, views.APIView):
"""
The base ViewSet class does not provide any actions by default.
diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py
index 5700e75b..46b80b24 100644
--- a/taiga/base/decorators.py
+++ b/taiga/base/decorators.py
@@ -18,6 +18,7 @@
from django_pglocks import advisory_lock
+
def detail_route(methods=['get'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail requests.
@@ -51,12 +52,11 @@ def model_pk_lock(func):
"""
def decorator(self, *args, **kwargs):
from taiga.base.utils.db import get_typename_for_model_class
- lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
pk = self.kwargs.get(self.pk_url_kwarg, None)
tn = get_typename_for_model_class(self.get_queryset().model)
key = "{0}:{1}".format(tn, pk)
- with advisory_lock(key) as acquired_key_lock:
+ with advisory_lock(key):
return func(self, *args, **kwargs)
return decorator
diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py
index cc58ee6d..73d277ff 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -51,6 +51,7 @@ In addition Django's built in 403 and 404 exceptions are handled.
"""
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
+from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.http import Http404
@@ -224,6 +225,7 @@ class NotEnoughSlotsForProject(BaseException):
"total_memberships": total_memberships
}
+
def format_exception(exc):
if isinstance(exc.detail, (dict, list, tuple,)):
detail = exc.detail
@@ -270,3 +272,6 @@ def exception_handler(exc):
# Note: Unhandled exceptions will raise a 500 error.
return None
+
+
+ValidationError = DjangoValidationError
diff --git a/taiga/base/fields.py b/taiga/base/fields.py
index 3f6fcf19..3b19f15f 100644
--- a/taiga/base/fields.py
+++ b/taiga/base/fields.py
@@ -18,13 +18,17 @@
from django.forms import widgets
from django.utils.translation import ugettext as _
+from taiga.base.api import serializers, ISO_8601
+from taiga.base.api.settings import api_settings
-from taiga.base.api import serializers
+import serpy
####################################################################
-## Serializer fields
+# DRF Serializer fields (OLD)
####################################################################
+# NOTE: This should be in other place, for example taiga.base.api.serializers
+
class JsonField(serializers.WritableField):
"""
@@ -39,40 +43,6 @@ class JsonField(serializers.WritableField):
return data
-class I18NJsonField(JsonField):
- """
- Json objects serializer.
- """
- widget = widgets.Textarea
-
- def __init__(self, i18n_fields=(), *args, **kwargs):
- super(I18NJsonField, self).__init__(*args, **kwargs)
- self.i18n_fields = i18n_fields
-
- def translate_values(self, d):
- i18n_d = {}
- if d is None:
- return d
-
- for key, value in d.items():
- if isinstance(value, dict):
- i18n_d[key] = self.translate_values(value)
-
- if key in self.i18n_fields:
- if isinstance(value, list):
- i18n_d[key] = [e is not None and _(str(e)) or e for e in value]
- if isinstance(value, str):
- i18n_d[key] = value is not None and _(value) or value
- else:
- i18n_d[key] = value
-
- return i18n_d
-
- def to_native(self, obj):
- i18n_obj = self.translate_values(obj)
- return i18n_obj
-
-
class PgArrayField(serializers.WritableField):
"""
PgArray objects serializer.
@@ -99,38 +69,81 @@ class PickledObjectField(serializers.WritableField):
return data
-class TagsField(serializers.WritableField):
- """
- Pickle objects serializer.
- """
- def to_native(self, obj):
- return obj
-
- def from_native(self, data):
- if not data:
- return data
-
- ret = sum([tag.split(",") for tag in data], [])
- return ret
-
-
-class TagsColorsField(serializers.WritableField):
- """
- PgArray objects serializer.
- """
- widget = widgets.Textarea
-
- def to_native(self, obj):
- return dict(obj)
-
- def from_native(self, data):
- return list(data.items())
-
-
-
class WatchersField(serializers.WritableField):
def to_native(self, obj):
return obj
def from_native(self, data):
return data
+
+
+####################################################################
+# Serpy fields (NEW)
+####################################################################
+
+class Field(serpy.Field):
+ pass
+
+
+class MethodField(serpy.MethodField):
+ pass
+
+
+class I18NField(Field):
+ def to_value(self, value):
+ ret = super(I18NField, self).to_value(value)
+ return _(ret)
+
+
+class I18NJsonField(Field):
+ """
+ Json objects serializer.
+ """
+ def __init__(self, i18n_fields=(), *args, **kwargs):
+ super(I18NJsonField, self).__init__(*args, **kwargs)
+ self.i18n_fields = i18n_fields
+
+ def translate_values(self, d):
+ i18n_d = {}
+ if d is None:
+ return d
+
+ for key, value in d.items():
+ if isinstance(value, dict):
+ i18n_d[key] = self.translate_values(value)
+
+ if key in self.i18n_fields:
+ if isinstance(value, list):
+ i18n_d[key] = [e is not None and _(str(e)) or e for e in value]
+ if isinstance(value, str):
+ i18n_d[key] = value is not None and _(value) or value
+ else:
+ i18n_d[key] = value
+
+ return i18n_d
+
+ def to_native(self, obj):
+ i18n_obj = self.translate_values(obj)
+ return i18n_obj
+
+
+class FileField(Field):
+ def to_value(self, value):
+ if value:
+ return value.name
+ return None
+
+
+class DateTimeField(Field):
+ format = api_settings.DATETIME_FORMAT
+
+ def to_value(self, value):
+ if value is None or self.format is None:
+ return value
+
+ if self.format.lower() == ISO_8601:
+ ret = value.isoformat()
+ if ret.endswith("+00:00"):
+ ret = ret[:-6] + "Z"
+ return ret
+ return value.strftime(self.format)
diff --git a/taiga/base/filters.py b/taiga/base/filters.py
index 1cd19e64..06274128 100644
--- a/taiga/base/filters.py
+++ b/taiga/base/filters.py
@@ -18,6 +18,8 @@
import logging
+from dateutil.parser import parse as parse_date
+
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
@@ -30,7 +32,6 @@ from taiga.base.utils.db import to_tsquery
logger = logging.getLogger(__name__)
-
#####################################################################
# Base and Mixins
#####################################################################
@@ -152,13 +153,17 @@ class PermissionBasedFilterBackend(FilterBackend):
else:
qs = qs.filter(project__anon_permissions__contains=[self.permission])
- return super().filter_queryset(request, qs.distinct(), view)
+ return super().filter_queryset(request, qs, view)
class CanViewProjectFilterBackend(PermissionBasedFilterBackend):
permission = "view_project"
+class CanViewEpicsFilterBackend(PermissionBasedFilterBackend):
+ permission = "view_epics"
+
+
class CanViewUsFilterBackend(PermissionBasedFilterBackend):
permission = "view_us"
@@ -197,6 +202,10 @@ class PermissionBasedAttachmentFilterBackend(PermissionBasedFilterBackend):
return qs.filter(content_type=ct)
+class CanViewEpicAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend):
+ permission = "view_epics"
+
+
class CanViewUserStoryAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend):
permission = "view_us"
@@ -229,7 +238,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
project_id = int(request.QUERY_PARAMS["project"])
except:
logger.error("Filtering project diferent value than an integer: {}".format(
- request.QUERY_PARAMS["project"]))
+ request.QUERY_PARAMS["project"]))
raise exc.BadRequest(_("'project' must be an integer value."))
if project_id:
@@ -256,14 +265,14 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
q = Q(memberships__project_id__in=projects_list) | Q(id=request.user.id)
- #If there is no selected project we want access to users from public projects
+ # If there is no selected project we want access to users from public projects
if not project:
q = q | Q(memberships__project__public_permissions__contains=[self.permission])
qs = qs.filter(q)
else:
- if project and not "view_project" in project.anon_permissions:
+ if project and "view_project" not in project.anon_permissions:
qs = qs.none()
qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission])
@@ -307,7 +316,7 @@ class IsProjectAdminFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend
else:
queryset = queryset.filter(project_id__in=project_ids)
- return super().filter_queryset(request, queryset.distinct(), view)
+ return super().filter_queryset(request, queryset, view)
class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend):
@@ -328,10 +337,16 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi
#####################################################################
class BaseRelatedFieldsFilter(FilterBackend):
- def __init__(self, filter_name=None):
+ filter_name = None
+ param_name = None
+
+ def __init__(self, filter_name=None, param_name=None):
if filter_name:
self.filter_name = filter_name
+ if param_name:
+ self.param_name = param_name
+
def _prepare_filter_data(self, query_param_value):
def _transform_value(value):
try:
@@ -346,7 +361,8 @@ class BaseRelatedFieldsFilter(FilterBackend):
return list(values)
def _get_queryparams(self, params):
- raw_value = params.get(self.filter_name, None)
+ param_name = self.param_name or self.filter_name
+ raw_value = params.get(param_name, None)
if raw_value:
value = self._prepare_filter_data(raw_value)
@@ -433,13 +449,14 @@ class WatchersFilter(FilterBackend):
def filter_queryset(self, request, queryset, view):
query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS)
- model = queryset.model
if query_watchers:
WatchedModel = apps.get_model("notifications", "Watched")
watched_type = ContentType.objects.get_for_model(queryset.model)
try:
- watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True)
+ watched_ids = (WatchedModel.objects.filter(content_type=watched_type,
+ user__id__in=query_watchers)
+ .values_list("object_id", flat=True))
queryset = queryset.filter(id__in=watched_ids)
except ValueError:
raise exc.BadRequest(_("Error in filter params types."))
@@ -447,6 +464,68 @@ class WatchersFilter(FilterBackend):
return super().filter_queryset(request, queryset, view)
+class BaseCompareFilter(FilterBackend):
+ operators = ["", "lt", "gt", "lte", "gte"]
+
+ def __init__(self, filter_name_base=None, operators=None):
+ if filter_name_base:
+ self.filter_name_base = filter_name_base
+
+ def _get_filter_names(self):
+ return [
+ self._get_filter_name(operator)
+ for operator in self.operators
+ ]
+
+ def _get_filter_name(self, operator):
+ if operator and len(operator) > 0:
+ return "{base}__{operator}".format(
+ base=self.filter_name_base, operator=operator
+ )
+ else:
+ return self.filter_name_base
+
+ def _get_constraints(self, params):
+ constraints = {}
+ for filter_name in self._get_filter_names():
+ raw_value = params.get(filter_name, None)
+ if raw_value is not None:
+ constraints[filter_name] = self._get_value(raw_value)
+ return constraints
+
+ def _get_value(self, raw_value):
+ return raw_value
+
+ def filter_queryset(self, request, queryset, view):
+ constraints = self._get_constraints(request.QUERY_PARAMS)
+
+ if len(constraints) > 0:
+ queryset = queryset.filter(**constraints)
+
+ return super().filter_queryset(request, queryset, view)
+
+
+class BaseDateFilter(BaseCompareFilter):
+ def _get_value(self, raw_value):
+ return parse_date(raw_value)
+
+
+class CreatedDateFilter(BaseDateFilter):
+ filter_name_base = "created_date"
+
+
+class ModifiedDateFilter(BaseDateFilter):
+ filter_name_base = "modified_date"
+
+
+class FinishedDateFilter(BaseDateFilter):
+ filter_name_base = "finished_date"
+
+
+class FinishDateFilter(BaseDateFilter):
+ filter_name_base = "finish_date"
+
+
#####################################################################
# Text search filters
#####################################################################
@@ -459,6 +538,7 @@ class QFilter(FilterBackend):
where_clause = ("""
to_tsvector('english_nostop',
coalesce({table}.subject, '') || ' ' ||
+ coalesce(array_to_string({table}.tags, ' '), '') || ' ' ||
coalesce({table}.ref) || ' ' ||
coalesce({table}.description, '')) @@ to_tsquery('english_nostop', %s)
""".format(table=table))
diff --git a/taiga/base/middleware/cors.py b/taiga/base/middleware/cors.py
index c7e2c615..3f5cbd38 100644
--- a/taiga/base/middleware/cors.py
+++ b/taiga/base/middleware/cors.py
@@ -25,7 +25,7 @@ COORS_ALLOWED_METHODS = ["POST", "GET", "OPTIONS", "PUT", "DELETE", "PATCH", "HE
COORS_ALLOWED_HEADERS = ["content-type", "x-requested-with",
"authorization", "accept-encoding",
"x-disable-pagination", "x-lazy-pagination",
- "x-host", "x-session-id"]
+ "x-host", "x-session-id", "set-orders"]
COORS_ALLOWED_CREDENTIALS = True
COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by",
"x-pagination-current", "x-pagination-next", "x-pagination-prev",
diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py
index a57d2eeb..c8733ade 100644
--- a/taiga/base/neighbors.py
+++ b/taiga/base/neighbors.py
@@ -23,6 +23,7 @@ from django.db import connection
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.sql.datastructures import EmptyResultSet
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
Neighbor = namedtuple("Neighbor", "left right")
@@ -71,7 +72,6 @@ def get_neighbors(obj, results_set=None):
if row is None:
return Neighbor(None, None)
- obj_position = row[1] - 1
left_object_id = row[2]
right_object_id = row[3]
@@ -88,13 +88,19 @@ def get_neighbors(obj, results_set=None):
return Neighbor(left, right)
-class NeighborsSerializerMixin:
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors")
+class NeighborSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+
+
+class NeighborsSerializerMixin(serializers.LightSerializer):
+ neighbors = MethodField()
def serialize_neighbor(self, neighbor):
- raise NotImplementedError
+ if neighbor:
+ return NeighborSerializer(neighbor).data
+ return None
def get_neighbors(self, obj):
view, request = self.context.get("view", None), self.context.get("request", None)
diff --git a/taiga/base/routers.py b/taiga/base/routers.py
index 56b80f8e..a7ccbdc4 100644
--- a/taiga/base/routers.py
+++ b/taiga/base/routers.py
@@ -318,7 +318,58 @@ class DRFDefaultRouter(SimpleRouter):
return urls
-class DefaultRouter(DRFDefaultRouter):
+class NestedRegistryItem(object):
+ def __init__(self, router, parent_prefix, parent_item=None):
+ self.router = router
+ self.parent_prefix = parent_prefix
+ self.parent_item = parent_item
+
+ def register(self, prefix, viewset, base_name, parents_query_lookups):
+ self.router._register(
+ prefix=self.get_prefix(current_prefix=prefix, parents_query_lookups=parents_query_lookups),
+ viewset=viewset,
+ base_name=base_name,
+ )
+ return NestedRegistryItem(
+ router=self.router,
+ parent_prefix=prefix,
+ parent_item=self
+ )
+
+ def get_prefix(self, current_prefix, parents_query_lookups):
+ return "{0}/{1}".format(
+ self.get_parent_prefix(parents_query_lookups),
+ current_prefix
+ )
+
+ def get_parent_prefix(self, parents_query_lookups):
+ prefix = "/"
+ current_item = self
+ i = len(parents_query_lookups) - 1
+ while current_item:
+ prefix = "{parent_prefix}/(?P<{parent_pk_kwarg_name}>[^/.]+)/{prefix}".format(
+ parent_prefix=current_item.parent_prefix,
+ parent_pk_kwarg_name=parents_query_lookups[i],
+ prefix=prefix
+ )
+ i -= 1
+ current_item = current_item.parent_item
+ return prefix.strip("/")
+
+
+class NestedRouterMixin:
+ def _register(self, *args, **kwargs):
+ return super().register(*args, **kwargs)
+
+ def register(self, *args, **kwargs):
+ self._register(*args, **kwargs)
+ return NestedRegistryItem(
+ router=self,
+ parent_prefix=self.registry[-1][0]
+ )
+
+
+class DefaultRouter(NestedRouterMixin, DRFDefaultRouter):
pass
__all__ = ["DefaultRouter"]
diff --git a/taiga/base/templates/emails/base-body-html.jinja b/taiga/base/templates/emails/base-body-html.jinja
index 57f331a5..ba857bb7 100644
--- a/taiga/base/templates/emails/base-body-html.jinja
+++ b/taiga/base/templates/emails/base-body-html.jinja
@@ -425,7 +425,7 @@
{{ support_url}}
Contact us:
-
+
{{ support_email }}
diff --git a/taiga/base/templates/emails/hero-body-html.jinja b/taiga/base/templates/emails/hero-body-html.jinja
index c88c7e5f..2f7d720e 100644
--- a/taiga/base/templates/emails/hero-body-html.jinja
+++ b/taiga/base/templates/emails/hero-body-html.jinja
@@ -399,7 +399,7 @@
{{ support_url}}
Contact us:
-
+
{{ support_email }}
diff --git a/taiga/base/templates/emails/updates-body-html.jinja b/taiga/base/templates/emails/updates-body-html.jinja
index af69858e..94d5b1ff 100644
--- a/taiga/base/templates/emails/updates-body-html.jinja
+++ b/taiga/base/templates/emails/updates-body-html.jinja
@@ -461,7 +461,7 @@
{{ support_url}}
Contact us:
-
+
{{ support_email }}
diff --git a/taiga/base/utils/collections.py b/taiga/base/utils/collections.py
new file mode 100644
index 00000000..c5ca3c59
--- /dev/null
+++ b/taiga/base/utils/collections.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import collections
+
+
+class OrderedSet(collections.MutableSet):
+ # Extract from:
+ # - https://docs.python.org/3/library/collections.abc.html?highlight=orderedset
+ # - https://code.activestate.com/recipes/576694/
+ def __init__(self, iterable=None):
+ self.end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.map = {} # key --> [key, prev, next]
+ if iterable is not None:
+ self |= iterable
+
+ def __len__(self):
+ return len(self.map)
+
+ def __contains__(self, key):
+ return key in self.map
+
+ def add(self, key):
+ if key not in self.map:
+ end = self.end
+ curr = end[1]
+ curr[2] = end[1] = self.map[key] = [key, curr, end]
+
+ def discard(self, key):
+ if key in self.map:
+ key, prev, next = self.map.pop(key)
+ prev[2] = next
+ next[1] = prev
+
+ def __iter__(self):
+ end = self.end
+ curr = end[2]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[2]
+
+ def __reversed__(self):
+ end = self.end
+ curr = end[1]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[1]
+
+ def pop(self, last=True):
+ if not self:
+ raise KeyError('set is empty')
+ key = self.end[1][0] if last else self.end[2][0]
+ self.discard(key)
+ return key
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, list(self))
+
+ def __eq__(self, other):
+ if isinstance(other, OrderedSet):
+ return len(self) == len(other) and list(self) == list(other)
+ return set(self) == set(other)
diff --git a/taiga/base/utils/colors.py b/taiga/base/utils/colors.py
new file mode 100644
index 00000000..517c8add
--- /dev/null
+++ b/taiga/base/utils/colors.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import random
+
+from django.conf import settings
+
+
+DEFAULT_PREDEFINED_COLORS = (
+ "#fce94f",
+ "#edd400",
+ "#c4a000",
+ "#8ae234",
+ "#73d216",
+ "#4e9a06",
+ "#d3d7cf",
+ "#fcaf3e",
+ "#f57900",
+ "#ce5c00",
+ "#729fcf",
+ "#3465a4",
+ "#204a87",
+ "#888a85",
+ "#ad7fa8",
+ "#75507b",
+ "#5c3566",
+ "#ef2929",
+ "#cc0000",
+ "#a40000"
+)
+
+PREDEFINED_COLORS = getattr(settings, "PREDEFINED_COLORS", DEFAULT_PREDEFINED_COLORS)
+
+
+def generate_random_hex_color():
+ return "#{:06x}".format(random.randint(0,0xFFFFFF))
+
+
+def generate_random_predefined_hex_color():
+ return random.choice(PREDEFINED_COLORS)
+
diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py
index 6569069d..eb610fff 100644
--- a/taiga/base/utils/db.py
+++ b/taiga/base/utils/db.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from django.contrib.contenttypes.models import ContentType
+from django.db import connection
from django.db import transaction
from django.shortcuts import _get_queryset
@@ -26,6 +27,7 @@ from . import functions
import re
+
def get_object_or_none(klass, *args, **kwargs):
"""
Uses get() to return an object, or None if the object does not exist.
@@ -81,6 +83,7 @@ def save_in_bulk(instances, callback=None, precall=None, **save_options):
:params callback: Callback to call after each save.
:params save_options: Additional options to use when saving each instance.
"""
+ ret = []
if callback is None:
callback = functions.noop
@@ -96,6 +99,7 @@ def save_in_bulk(instances, callback=None, precall=None, **save_options):
instance.save(**save_options)
callback(instance, created=created)
+ return ret
@transaction.atomic
def update_in_bulk(instances, list_of_new_values, callback=None, precall=None):
@@ -119,19 +123,28 @@ def update_in_bulk(instances, list_of_new_values, callback=None, precall=None):
callback(instance)
-def update_in_bulk_with_ids(ids, list_of_new_values, model):
+def update_attr_in_bulk_for_ids(values, attr, model):
"""Update a table using a list of ids.
- :params ids: List of ids.
- :params new_values: List of dicts or duples where each dict/duple is the new data corresponding
- to the instance in the same index position as the dict.
- :param model: Model of the ids.
+ :params values: Dict of new values where the key is the pk of the element to update.
+ :params attr: attr to update
+ :params model: Model of the ids.
"""
- tn = get_typename_for_model_class(model)
- for id, new_values in zip(ids, list_of_new_values):
- key = "{0}:{1}".format(tn, id)
- with advisory_lock(key) as acquired_key_lock:
- model.objects.filter(id=id).update(**new_values)
+ values = [str((id, order)) for id, order in values.items()]
+ sql = """
+ UPDATE "{tbl}"
+ SET "{attr}"=update_values.column2
+ FROM (
+ VALUES
+ {values}
+ ) AS update_values
+ WHERE "{tbl}"."id"=update_values.column1;
+ """.format(tbl=model._meta.db_table,
+ values=', '.join(values),
+ attr=attr)
+
+ cursor = connection.cursor()
+ cursor.execute(sql)
def to_tsquery(term):
diff --git a/taiga/base/utils/dicts.py b/taiga/base/utils/dicts.py
index 23b90f17..bf3d2c71 100644
--- a/taiga/base/utils/dicts.py
+++ b/taiga/base/utils/dicts.py
@@ -25,3 +25,7 @@ def dict_sum(*args):
assert isinstance(arg, dict)
result += collections.Counter(arg)
return result
+
+
+def into_namedtuple(dictionary):
+ return collections.namedtuple('GenericDict', dictionary.keys())(**dictionary)
diff --git a/taiga/base/utils/time.py b/taiga/base/utils/time.py
new file mode 100644
index 00000000..cd7b00c4
--- /dev/null
+++ b/taiga/base/utils/time.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import time
+
+
+def timestamp_ms():
+ return int(time.time() * 1000)
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index d8453ad5..75644365 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -34,6 +34,7 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base.api.mixins import CreateModelMixin
from taiga.base.api.viewsets import GenericViewSet
+from taiga.projects import utils as project_utils
from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
@@ -43,11 +44,11 @@ from taiga.users import services as users_services
from . import exceptions as err
from . import mixins
from . import permissions
+from . import validators
from . import serializers
from . import services
from . import tasks
from . import throttling
-from .renderers import ExportRenderer
from taiga.base.api.utils import get_object_or_404
@@ -75,13 +76,11 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet)
if dump_format == "gzip":
path = "exports/{}/{}-{}.json.gz".format(project.pk, project.slug, uuid.uuid4().hex)
- storage_path = default_storage.path(path)
- with default_storage.open(storage_path, mode="wb") as outfile:
+ with default_storage.open(path, mode="wb") as outfile:
services.render_project(project, gzip.GzipFile(fileobj=outfile))
else:
path = "exports/{}/{}-{}.json".format(project.pk, project.slug, uuid.uuid4().hex)
- storage_path = default_storage.path(path)
- with default_storage.open(storage_path, mode="wb") as outfile:
+ with default_storage.open(path, mode="wb") as outfile:
services.render_project(project, outfile)
response_data = {
@@ -103,9 +102,8 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Validate if the project can be imported
is_private = data.get('is_private', False)
- total_memberships = len([m for m in data.get("memberships", [])
- if m.get("email", None) != data["owner"]])
- total_memberships = total_memberships + 1 # 1 is the owner
+ total_memberships = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
+ total_memberships = total_memberships + 1 # 1 is the owner
(enough_slots, error_message) = users_services.has_available_slot_for_import_new_project(
self.request.user,
is_private,
@@ -148,31 +146,31 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Create project values choicess
if "points" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
- "points", serializers.PointsExportSerializer)
+ "points", validators.PointsExportValidator)
if "issue_types" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
"issue_types",
- serializers.IssueTypeExportSerializer)
+ validators.IssueTypeExportValidator)
if "issue_statuses" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
"issue_statuses",
- serializers.IssueStatusExportSerializer,)
+ validators.IssueStatusExportValidator,)
if "us_statuses" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
"us_statuses",
- serializers.UserStoryStatusExportSerializer,)
+ validators.UserStoryStatusExportValidator,)
if "task_statuses" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
"task_statuses",
- serializers.TaskStatusExportSerializer)
+ validators.TaskStatusExportValidator)
if "priorities" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
"priorities",
- serializers.PriorityExportSerializer)
+ validators.PriorityExportValidator)
if "severities" in data:
services.store.store_project_attributes_values(project_serialized.object, data,
"severities",
- serializers.SeverityExportSerializer)
+ validators.SeverityExportValidator)
if ("points" in data or "issues_types" in data or
"issues_statuses" in data or "us_statuses" in data or
@@ -184,17 +182,17 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if "userstorycustomattributes" in data:
services.store.store_custom_attributes(project_serialized.object, data,
"userstorycustomattributes",
- serializers.UserStoryCustomAttributeExportSerializer)
+ validators.UserStoryCustomAttributeExportValidator)
if "taskcustomattributes" in data:
services.store.store_custom_attributes(project_serialized.object, data,
"taskcustomattributes",
- serializers.TaskCustomAttributeExportSerializer)
+ validators.TaskCustomAttributeExportValidator)
if "issuecustomattributes" in data:
services.store.store_custom_attributes(project_serialized.object, data,
"issuecustomattributes",
- serializers.IssueCustomAttributeExportSerializer)
+ validators.IssueCustomAttributeExportValidator)
# Is there any error?
errors = services.store.get_errors()
@@ -202,7 +200,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
raise exc.BadRequest(errors)
# Importer process is OK
- response_data = project_serialized.data
+ response_data = serializers.ProjectExportSerializer(project_serialized.object).data
response_data['id'] = project_serialized.object.id
headers = self.get_success_headers(response_data)
return response.Created(response_data, headers=headers)
@@ -219,8 +217,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(milestone.data)
- return response.Created(milestone.data, headers=headers)
+ data = serializers.MilestoneExportSerializer(milestone.object).data
+ headers = self.get_success_headers(data)
+ return response.Created(data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -234,8 +233,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(us.data)
- return response.Created(us.data, headers=headers)
+ data = serializers.UserStoryExportSerializer(us.object).data
+ headers = self.get_success_headers(data)
+ return response.Created(data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -252,8 +252,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(task.data)
- return response.Created(task.data, headers=headers)
+ data = serializers.TaskExportSerializer(task.object).data
+ headers = self.get_success_headers(data)
+ return response.Created(data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -270,8 +271,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(issue.data)
- return response.Created(issue.data, headers=headers)
+ data = serializers.IssueExportSerializer(issue.object).data
+ headers = self.get_success_headers(data)
+ return response.Created(data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -285,8 +287,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(wiki_page.data)
- return response.Created(wiki_page.data, headers=headers)
+ data = serializers.WikiPageExportSerializer(wiki_page.object).data
+ headers = self.get_success_headers(data)
+ return response.Created(data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -300,8 +303,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(wiki_link.data)
- return response.Created(wiki_link.data, headers=headers)
+ data = serializers.WikiLinkExportSerializer(wiki_link.object).data
+ headers = self.get_success_headers(data)
+ return response.Created(data, headers=headers)
@list_route(methods=["POST"])
@method_decorator(atomic)
@@ -366,5 +370,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
return response.BadRequest({"error": e.message, "details": e.errors})
else:
# On Success
- response_data = ProjectSerializer(project).data
+ project_from_qs = project_utils.attach_extra_info(Project.objects.all()).get(id=project.id)
+ response_data = ProjectSerializer(project_from_qs).data
+
return response.Created(response_data)
diff --git a/taiga/export_import/management/commands/dump_project_async.py b/taiga/export_import/management/commands/dump_project_async.py
index 8052e538..d48a0c19 100644
--- a/taiga/export_import/management/commands/dump_project_async.py
+++ b/taiga/export_import/management/commands/dump_project_async.py
@@ -22,7 +22,7 @@ from django.conf import settings
from taiga.projects.models import Project
from taiga.users.models import User
-from taiga.permissions.service import is_project_admin
+from taiga.permissions.services import is_project_admin
from taiga.export_import import tasks
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index 8a4ca585..c01f577f 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -50,24 +50,27 @@ class Command(BaseCommand):
data = json.loads(open(dump_file_path, 'r').read())
try:
- with transaction.atomic():
- if overwrite:
- receivers_back = signals.post_delete.receivers
- signals.post_delete.receivers = []
- try:
- proj = Project.objects.get(slug=data.get("slug", "not a slug"))
- proj.tasks.all().delete()
- proj.user_stories.all().delete()
- proj.issues.all().delete()
- proj.memberships.all().delete()
- proj.roles.all().delete()
- proj.delete()
- except Project.DoesNotExist:
- pass
- signals.post_delete.receivers = receivers_back
+ if overwrite:
+ receivers_back = signals.post_delete.receivers
+ signals.post_delete.receivers = []
+ try:
+ proj = Project.objects.get(slug=data.get("slug", "not a slug"))
+ proj.tasks.all().delete()
+ proj.user_stories.all().delete()
+ proj.issues.all().delete()
+ proj.memberships.all().delete()
+ proj.roles.all().delete()
+ proj.delete()
+ except Project.DoesNotExist:
+ pass
+ signals.post_delete.receivers = receivers_back
+ else:
+ slug = data.get('slug', None)
+ if slug is not None and Project.objects.filter(slug=slug).exists():
+ del data['slug']
- user = User.objects.get(email=owner_email)
- services.store_project_from_dict(data, user)
+ user = User.objects.get(email=owner_email)
+ services.store_project_from_dict(data, user)
except err.TaigaImportError as e:
if e.project:
e.project.delete_related_content()
diff --git a/taiga/export_import/serializers/cache.py b/taiga/export_import/serializers/cache.py
index c4eb5bfa..f22978f8 100644
--- a/taiga/export_import/serializers/cache.py
+++ b/taiga/export_import/serializers/cache.py
@@ -23,7 +23,7 @@ _cache_user_by_email = {}
_custom_tasks_attributes_cache = {}
_custom_issues_attributes_cache = {}
_custom_userstories_attributes_cache = {}
-
+_custom_epics_attributes_cache = {}
def cached_get_user_by_pk(pk):
if pk not in _cache_user_by_pk:
diff --git a/taiga/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py
index 64c01436..29ec85aa 100644
--- a/taiga/export_import/serializers/fields.py
+++ b/taiga/export_import/serializers/fields.py
@@ -21,24 +21,15 @@ import os
import copy
from collections import OrderedDict
-from django.core.files.base import ContentFile
-from django.core.exceptions import ObjectDoesNotExist
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext as _
-from django.contrib.contenttypes.models import ContentType
-
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
-from taiga.mdrender.service import render as mdrender
+from taiga.base.fields import Field
from taiga.users import models as users_models
-from .cache import cached_get_user_by_email, cached_get_user_by_pk
+from .cache import cached_get_user_by_pk
-class FileField(serializers.WritableField):
- read_only = False
-
- def to_native(self, obj):
+class FileField(Field):
+ def to_value(self, obj):
if not obj:
return None
@@ -49,202 +40,74 @@ class FileField(serializers.WritableField):
("name", os.path.basename(obj.name)),
])
- def from_native(self, data):
- if not data:
- return None
- decoded_data = b''
- # The original file was encoded by chunks but we don't really know its
- # length or if it was multiple of 3 so we must iterate over all those chunks
- # decoding them one by one
- for decoding_chunk in data['data'].split("="):
- # When encoding to base64 3 bytes are transformed into 4 bytes and
- # the extra space of the block is filled with =
- # We must ensure that the decoding chunk has a length multiple of 4 so
- # we restore the stripped '='s adding appending them until the chunk has
- # a length multiple of 4
- decoding_chunk += "=" * (-len(decoding_chunk) % 4)
- decoded_data += base64.b64decode(decoding_chunk+"=")
-
- return ContentFile(decoded_data, name=data['name'])
-
-
-class ContentTypeField(serializers.RelatedField):
- read_only = False
-
- def to_native(self, obj):
+class ContentTypeField(Field):
+ def to_value(self, obj):
if obj:
return [obj.app_label, obj.model]
return None
- def from_native(self, data):
- try:
- return ContentType.objects.get_by_natural_key(*data)
- except Exception:
- return None
-
-class RelatedNoneSafeField(serializers.RelatedField):
- def field_from_native(self, data, files, field_name, into):
- if self.read_only:
- return
-
- try:
- if self.many:
- try:
- # Form data
- value = data.getlist(field_name)
- if value == [''] or value == []:
- raise KeyError
- except AttributeError:
- # Non-form data
- value = data[field_name]
- else:
- value = data[field_name]
- except KeyError:
- if self.partial:
- return
- value = self.get_default_value()
-
- key = self.source or field_name
- if value in self.null_values:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- into[key] = None
- elif self.many:
- into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None]
- else:
- into[key] = self.from_native(value)
-
-
-class UserRelatedField(RelatedNoneSafeField):
- read_only = False
-
- def to_native(self, obj):
+class UserRelatedField(Field):
+ def to_value(self, obj):
if obj:
return obj.email
return None
- def from_native(self, data):
- try:
- return cached_get_user_by_email(data)
- except users_models.User.DoesNotExist:
- return None
-
-class UserPkField(serializers.RelatedField):
- read_only = False
-
- def to_native(self, obj):
+class UserPkField(Field):
+ def to_value(self, obj):
try:
user = cached_get_user_by_pk(obj)
return user.email
except users_models.User.DoesNotExist:
return None
- def from_native(self, data):
- try:
- user = cached_get_user_by_email(data)
- return user.pk
- except users_models.User.DoesNotExist:
- return None
-
-
-class CommentField(serializers.WritableField):
- read_only = False
-
- def field_from_native(self, data, files, field_name, into):
- super().field_from_native(data, files, field_name, into)
- into["comment_html"] = mdrender(self.context['project'], data.get("comment", ""))
-
-
-class ProjectRelatedField(serializers.RelatedField):
- read_only = False
- null_values = (None, "")
+class SlugRelatedField(Field):
def __init__(self, slug_field, *args, **kwargs):
self.slug_field = slug_field
super().__init__(*args, **kwargs)
- def to_native(self, obj):
+ def to_value(self, obj):
if obj:
return getattr(obj, self.slug_field)
return None
- def from_native(self, data):
- try:
- kwargs = {self.slug_field: data, "project": self.context['project']}
- return self.queryset.get(**kwargs)
- except ObjectDoesNotExist:
- raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data)))
-
-class HistoryUserField(JsonField):
- def to_native(self, obj):
+class HistoryUserField(Field):
+ def to_value(self, obj):
if obj is None or obj == {}:
return []
try:
user = cached_get_user_by_pk(obj['pk'])
except users_models.User.DoesNotExist:
user = None
- return (UserRelatedField().to_native(user), obj['name'])
-
- def from_native(self, data):
- if data is None:
- return {}
-
- if len(data) < 2:
- return {}
-
- user = UserRelatedField().from_native(data[0])
-
- if user:
- pk = user.pk
- else:
- pk = None
-
- return {"pk": pk, "name": data[1]}
+ return (UserRelatedField().to_value(user), obj['name'])
-class HistoryValuesField(JsonField):
- def to_native(self, obj):
+class HistoryValuesField(Field):
+ def to_value(self, obj):
if obj is None:
return []
if "users" in obj:
- obj['users'] = list(map(UserPkField().to_native, obj['users']))
+ obj['users'] = list(map(UserPkField().to_value, obj['users']))
return obj
- def from_native(self, data):
- if data is None:
- return []
- if "users" in data:
- data['users'] = list(map(UserPkField().from_native, data['users']))
- return data
-
-class HistoryDiffField(JsonField):
- def to_native(self, obj):
+class HistoryDiffField(Field):
+ def to_value(self, obj):
if obj is None:
return []
if "assigned_to" in obj:
- obj['assigned_to'] = list(map(UserPkField().to_native, obj['assigned_to']))
+ obj['assigned_to'] = list(map(UserPkField().to_value, obj['assigned_to']))
return obj
- def from_native(self, data):
- if data is None:
- return []
- if "assigned_to" in data:
- data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to']))
- return data
-
-
-class TimelineDataField(serializers.WritableField):
- read_only = False
-
- def to_native(self, data):
+class TimelineDataField(Field):
+ def to_value(self, data):
new_data = copy.deepcopy(data)
try:
user = cached_get_user_by_pk(new_data["user"]["id"])
@@ -253,14 +116,3 @@ class TimelineDataField(serializers.WritableField):
except Exception:
pass
return new_data
-
- def from_native(self, data):
- new_data = copy.deepcopy(data)
- try:
- user = cached_get_user_by_email(new_data["user"]["email"])
- new_data["user"]["id"] = user.id
- del new_data["user"]["email"]
- except users_models.User.DoesNotExist:
- pass
-
- return new_data
diff --git a/taiga/export_import/serializers/mixins.py b/taiga/export_import/serializers/mixins.py
index 007649a2..3006500f 100644
--- a/taiga/export_import/serializers/mixins.py
+++ b/taiga/export_import/serializers/mixins.py
@@ -16,56 +16,62 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField, DateTimeField
from taiga.projects.history import models as history_models
from taiga.projects.attachments import models as attachments_models
-from taiga.projects.notifications import services as notifications_services
from taiga.projects.history import services as history_service
from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField,
- JsonField, HistoryValuesField, CommentField, FileField)
+ HistoryValuesField, FileField)
-class HistoryExportSerializer(serializers.ModelSerializer):
+class HistoryExportSerializer(serializers.LightSerializer):
user = HistoryUserField()
- diff = HistoryDiffField(required=False)
- snapshot = JsonField(required=False)
- values = HistoryValuesField(required=False)
- comment = CommentField(required=False)
- delete_comment_date = serializers.DateTimeField(required=False)
- delete_comment_user = HistoryUserField(required=False)
-
- class Meta:
- model = history_models.HistoryEntry
- exclude = ("id", "comment_html", "key")
+ diff = HistoryDiffField()
+ snapshot = Field()
+ values = HistoryValuesField()
+ comment = Field()
+ delete_comment_date = DateTimeField()
+ delete_comment_user = HistoryUserField()
+ comment_versions = Field()
+ created_at = DateTimeField()
+ edit_comment_date = DateTimeField()
+ is_hidden = Field()
+ is_snapshot = Field()
+ type = Field()
-class HistoryExportSerializerMixin(serializers.ModelSerializer):
- history = serializers.SerializerMethodField("get_history")
+class HistoryExportSerializerMixin(serializers.LightSerializer):
+ history = MethodField("get_history")
def get_history(self, obj):
- history_qs = history_service.get_history_queryset_by_model_instance(obj,
- types=(history_models.HistoryType.change, history_models.HistoryType.create,))
+ history_qs = history_service.get_history_queryset_by_model_instance(
+ obj,
+ types=(history_models.HistoryType.change, history_models.HistoryType.create,)
+ )
return HistoryExportSerializer(history_qs, many=True).data
-class AttachmentExportSerializer(serializers.ModelSerializer):
- owner = UserRelatedField(required=False)
+class AttachmentExportSerializer(serializers.LightSerializer):
+ owner = UserRelatedField()
attached_file = FileField()
- modified_date = serializers.DateTimeField(required=False)
-
- class Meta:
- model = attachments_models.Attachment
- exclude = ('id', 'content_type', 'object_id', 'project')
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
+ description = Field()
+ is_deprecated = Field()
+ name = Field()
+ order = Field()
+ sha1 = Field()
+ size = Field()
-class AttachmentExportSerializerMixin(serializers.ModelSerializer):
- attachments = serializers.SerializerMethodField("get_attachments")
+class AttachmentExportSerializerMixin(serializers.LightSerializer):
+ attachments = MethodField()
def get_attachments(self, obj):
content_type = ContentType.objects.get_for_model(obj.__class__)
@@ -74,8 +80,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer):
return AttachmentExportSerializer(attachments_qs, many=True).data
-class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer):
- custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
+class CustomAttributesValuesExportSerializerMixin(serializers.LightSerializer):
+ custom_attributes_values = MethodField("get_custom_attributes_values")
def custom_attributes_queryset(self, project):
raise NotImplementedError()
@@ -85,13 +91,13 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer):
ret = {}
for attr in custom_attributes:
value = values.get(str(attr["id"]), None)
- if value is not None:
+ if value is not None:
ret[attr["name"]] = value
return ret
try:
- values = obj.custom_attributes_values.attributes_values
+ values = obj.custom_attributes_values.attributes_values
custom_attributes = self.custom_attributes_queryset(obj.project)
return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values)
@@ -99,43 +105,8 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer):
return None
-class WatcheableObjectModelSerializerMixin(serializers.ModelSerializer):
- watchers = UserRelatedField(many=True, required=False)
+class WatcheableObjectLightSerializerMixin(serializers.LightSerializer):
+ watchers = MethodField()
- def __init__(self, *args, **kwargs):
- self._watchers_field = self.base_fields.pop("watchers", None)
- super(WatcheableObjectModelSerializerMixin, self).__init__(*args, **kwargs)
-
- """
- watchers is not a field from the model so we need to do some magic to make it work like a normal field
- It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
- """
-
- def restore_object(self, attrs, instance=None):
- watcher_field = self.fields.pop("watchers", None)
- instance = super(WatcheableObjectModelSerializerMixin, self).restore_object(attrs, instance)
- self._watchers = self.init_data.get("watchers", [])
- return instance
-
- def save_watchers(self):
- new_watcher_emails = set(self._watchers)
- old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True))
- adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
- removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
-
- User = get_user_model()
- adding_users = User.objects.filter(email__in=adding_watcher_emails)
- removing_users = User.objects.filter(email__in=removing_watcher_emails)
-
- for user in adding_users:
- notifications_services.add_watcher(self.object, user)
-
- for user in removing_users:
- notifications_services.remove_watcher(self.object, user)
-
- self.object.watchers = [user.email for user in self.object.get_watchers()]
-
- def to_native(self, obj):
- ret = super(WatcheableObjectModelSerializerMixin, self).to_native(obj)
- ret["watchers"] = [user.email for user in obj.get_watchers()]
- return ret
+ def get_watchers(self, obj):
+ return [user.email for user in obj.get_watchers()]
diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py
index 7cf46cba..f4f46e52 100644
--- a/taiga/export_import/serializers/serializers.py
+++ b/taiga/export_import/serializers/serializers.py
@@ -16,235 +16,201 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import copy
-
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext as _
-
from taiga.base.api import serializers
-from taiga.base.fields import JsonField, PgArrayField
+from taiga.base.fields import Field, DateTimeField, MethodField
-from taiga.projects import models as projects_models
-from taiga.projects.custom_attributes import models as custom_attributes_models
-from taiga.projects.userstories import models as userstories_models
-from taiga.projects.tasks import models as tasks_models
-from taiga.projects.issues import models as issues_models
-from taiga.projects.milestones import models as milestones_models
-from taiga.projects.wiki import models as wiki_models
-from taiga.projects.history import models as history_models
-from taiga.projects.attachments import models as attachments_models
-from taiga.timeline import models as timeline_models
-from taiga.users import models as users_models
from taiga.projects.votes import services as votes_service
-from .fields import (FileField, RelatedNoneSafeField, UserRelatedField,
- UserPkField, CommentField, ProjectRelatedField,
- HistoryUserField, HistoryValuesField, HistoryDiffField,
- TimelineDataField, ContentTypeField)
+from .fields import (FileField, UserRelatedField, TimelineDataField,
+ ContentTypeField, SlugRelatedField)
from .mixins import (HistoryExportSerializerMixin,
AttachmentExportSerializerMixin,
CustomAttributesValuesExportSerializerMixin,
- WatcheableObjectModelSerializerMixin)
+ WatcheableObjectLightSerializerMixin)
from .cache import (_custom_tasks_attributes_cache,
_custom_userstories_attributes_cache,
+ _custom_epics_attributes_cache,
_custom_issues_attributes_cache)
-class PointsExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.Points
- exclude = ('id', 'project')
+class RelatedExportSerializer(serializers.LightSerializer):
+ def to_value(self, value):
+ if hasattr(value, 'all'):
+ return super().to_value(value.all())
+ return super().to_value(value)
-class UserStoryStatusExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.UserStoryStatus
- exclude = ('id', 'project')
+class PointsExportSerializer(RelatedExportSerializer):
+ name = Field()
+ order = Field()
+ value = Field()
-class TaskStatusExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.TaskStatus
- exclude = ('id', 'project')
+class UserStoryStatusExportSerializer(RelatedExportSerializer):
+ name = Field()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ is_archived = Field()
+ color = Field()
+ wip_limit = Field()
-class IssueStatusExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.IssueStatus
- exclude = ('id', 'project')
+class EpicStatusExportSerializer(RelatedExportSerializer):
+ name = Field()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
-class PriorityExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.Priority
- exclude = ('id', 'project')
+class TaskStatusExportSerializer(RelatedExportSerializer):
+ name = Field()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
-class SeverityExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.Severity
- exclude = ('id', 'project')
+class IssueStatusExportSerializer(RelatedExportSerializer):
+ name = Field()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
-class IssueTypeExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = projects_models.IssueType
- exclude = ('id', 'project')
+class PriorityExportSerializer(RelatedExportSerializer):
+ name = Field()
+ order = Field()
+ color = Field()
-class RoleExportSerializer(serializers.ModelSerializer):
- permissions = PgArrayField(required=False)
-
- class Meta:
- model = users_models.Role
- exclude = ('id', 'project')
+class SeverityExportSerializer(RelatedExportSerializer):
+ name = Field()
+ order = Field()
+ color = Field()
-class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer):
- modified_date = serializers.DateTimeField(required=False)
-
- class Meta:
- model = custom_attributes_models.UserStoryCustomAttribute
- exclude = ('id', 'project')
+class IssueTypeExportSerializer(RelatedExportSerializer):
+ name = Field()
+ order = Field()
+ color = Field()
-class TaskCustomAttributeExportSerializer(serializers.ModelSerializer):
- modified_date = serializers.DateTimeField(required=False)
-
- class Meta:
- model = custom_attributes_models.TaskCustomAttribute
- exclude = ('id', 'project')
+class RoleExportSerializer(RelatedExportSerializer):
+ name = Field()
+ slug = Field()
+ order = Field()
+ computable = Field()
+ permissions = Field()
-class IssueCustomAttributeExportSerializer(serializers.ModelSerializer):
- modified_date = serializers.DateTimeField(required=False)
-
- class Meta:
- model = custom_attributes_models.IssueCustomAttribute
- exclude = ('id', 'project')
+class EpicCustomAttributesExportSerializer(RelatedExportSerializer):
+ name = Field()
+ description = Field()
+ type = Field()
+ order = Field()
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
-class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
- attributes_values = JsonField(source="attributes_values",required=True)
- _custom_attribute_model = None
- _container_field = None
+class UserStoryCustomAttributeExportSerializer(RelatedExportSerializer):
+ name = Field()
+ description = Field()
+ type = Field()
+ order = Field()
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
- class Meta:
- exclude = ("id",)
- def validate_attributes_values(self, attrs, source):
- # values must be a dict
- data_values = attrs.get("attributes_values", None)
- if self.object:
- data_values = (data_values or self.object.attributes_values)
+class TaskCustomAttributeExportSerializer(RelatedExportSerializer):
+ name = Field()
+ description = Field()
+ type = Field()
+ order = Field()
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
- if type(data_values) is not dict:
- raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
- # Values keys must be in the container object project
- data_container = attrs.get(self._container_field, None)
- if data_container:
- project_id = data_container.project_id
- elif self.object:
- project_id = getattr(self.object, self._container_field).project_id
- else:
- project_id = None
+class IssueCustomAttributeExportSerializer(RelatedExportSerializer):
+ name = Field()
+ description = Field()
+ type = Field()
+ order = Field()
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
- values_ids = list(data_values.keys())
- qs = self._custom_attribute_model.objects.filter(project=project_id,
- id__in=values_ids)
- if qs.count() != len(values_ids):
- raise ValidationError(_("It contain invalid custom fields."))
- return attrs
+class BaseCustomAttributesValuesExportSerializer(RelatedExportSerializer):
+ attributes_values = Field(required=True)
+
class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
- _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
- _container_model = "userstories.UserStory"
- _container_field = "user_story"
-
- class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
- model = custom_attributes_models.UserStoryCustomAttributesValues
+ user_story = Field(attr="user_story.id")
class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
- _custom_attribute_model = custom_attributes_models.TaskCustomAttribute
- _container_field = "task"
-
- class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
- model = custom_attributes_models.TaskCustomAttributesValues
+ task = Field(attr="task.id")
class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
- _custom_attribute_model = custom_attributes_models.IssueCustomAttribute
- _container_field = "issue"
-
- class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
- model = custom_attributes_models.IssueCustomAttributesValues
+ issue = Field(attr="issue.id")
-class MembershipExportSerializer(serializers.ModelSerializer):
- user = UserRelatedField(required=False)
- role = ProjectRelatedField(slug_field="name")
- invited_by = UserRelatedField(required=False)
-
- class Meta:
- model = projects_models.Membership
- exclude = ('id', 'project', 'token')
-
- def full_clean(self, instance):
- return instance
+class MembershipExportSerializer(RelatedExportSerializer):
+ user = UserRelatedField()
+ role = SlugRelatedField(slug_field="name")
+ invited_by = UserRelatedField()
+ is_admin = Field()
+ email = Field()
+ created_at = DateTimeField()
+ invitation_extra_text = Field()
+ user_order = Field()
-class RolePointsExportSerializer(serializers.ModelSerializer):
- role = ProjectRelatedField(slug_field="name")
- points = ProjectRelatedField(slug_field="name")
-
- class Meta:
- model = userstories_models.RolePoints
- exclude = ('id', 'user_story')
+class RolePointsExportSerializer(RelatedExportSerializer):
+ role = SlugRelatedField(slug_field="name")
+ points = SlugRelatedField(slug_field="name")
-class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin):
- owner = UserRelatedField(required=False)
- modified_date = serializers.DateTimeField(required=False)
- estimated_start = serializers.DateField(required=False)
- estimated_finish = serializers.DateField(required=False)
-
- def __init__(self, *args, **kwargs):
- project = kwargs.pop('project', None)
- super(MilestoneExportSerializer, self).__init__(*args, **kwargs)
- if project:
- self.project = project
-
- def validate_name(self, attrs, source):
- """
- Check the milestone name is not duplicated in the project
- """
- name = attrs[source]
- qs = self.project.milestones.filter(name=name)
- if qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
-
- class Meta:
- model = milestones_models.Milestone
- exclude = ('id', 'project')
+class MilestoneExportSerializer(WatcheableObjectLightSerializerMixin, RelatedExportSerializer):
+ name = Field()
+ owner = UserRelatedField()
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
+ estimated_start = Field()
+ estimated_finish = Field()
+ slug = Field()
+ closed = Field()
+ disponibility = Field()
+ order = Field()
-class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
- AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin):
- owner = UserRelatedField(required=False)
- status = ProjectRelatedField(slug_field="name")
- user_story = ProjectRelatedField(slug_field="ref", required=False)
- milestone = ProjectRelatedField(slug_field="name", required=False)
- assigned_to = UserRelatedField(required=False)
- modified_date = serializers.DateTimeField(required=False)
-
- class Meta:
- model = tasks_models.Task
- exclude = ('id', 'project')
+class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin,
+ HistoryExportSerializerMixin,
+ AttachmentExportSerializerMixin,
+ WatcheableObjectLightSerializerMixin,
+ RelatedExportSerializer):
+ owner = UserRelatedField()
+ status = SlugRelatedField(slug_field="name")
+ user_story = SlugRelatedField(slug_field="ref")
+ milestone = SlugRelatedField(slug_field="name")
+ assigned_to = UserRelatedField()
+ modified_date = DateTimeField()
+ created_date = DateTimeField()
+ finished_date = DateTimeField()
+ ref = Field()
+ subject = Field()
+ us_order = Field()
+ taskboard_order = Field()
+ description = Field()
+ is_iocaine = Field()
+ external_reference = Field()
+ version = Field()
+ blocked_note = Field()
+ is_blocked = Field()
+ tags = Field()
def custom_attributes_queryset(self, project):
if project.id not in _custom_tasks_attributes_cache:
@@ -252,41 +218,108 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE
return _custom_tasks_attributes_cache[project.id]
-class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
- AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin):
- role_points = RolePointsExportSerializer(many=True, required=False)
- owner = UserRelatedField(required=False)
- assigned_to = UserRelatedField(required=False)
- status = ProjectRelatedField(slug_field="name")
- milestone = ProjectRelatedField(slug_field="name", required=False)
- modified_date = serializers.DateTimeField(required=False)
- generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
-
- class Meta:
- model = userstories_models.UserStory
- exclude = ('id', 'project', 'points', 'tasks')
+class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin,
+ HistoryExportSerializerMixin,
+ AttachmentExportSerializerMixin,
+ WatcheableObjectLightSerializerMixin,
+ RelatedExportSerializer):
+ role_points = RolePointsExportSerializer(many=True)
+ owner = UserRelatedField()
+ assigned_to = UserRelatedField()
+ status = SlugRelatedField(slug_field="name")
+ milestone = SlugRelatedField(slug_field="name")
+ modified_date = DateTimeField()
+ created_date = DateTimeField()
+ finish_date = DateTimeField()
+ generated_from_issue = SlugRelatedField(slug_field="ref")
+ ref = Field()
+ is_closed = Field()
+ backlog_order = Field()
+ sprint_order = Field()
+ kanban_order = Field()
+ subject = Field()
+ description = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ external_reference = Field()
+ tribe_gig = Field()
+ version = Field()
+ blocked_note = Field()
+ is_blocked = Field()
+ tags = Field()
def custom_attributes_queryset(self, project):
if project.id not in _custom_userstories_attributes_cache:
- _custom_userstories_attributes_cache[project.id] = list(project.userstorycustomattributes.all().values('id', 'name'))
+ _custom_userstories_attributes_cache[project.id] = list(
+ project.userstorycustomattributes.all().values('id', 'name')
+ )
return _custom_userstories_attributes_cache[project.id]
-class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
- AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin):
- owner = UserRelatedField(required=False)
- status = ProjectRelatedField(slug_field="name")
- assigned_to = UserRelatedField(required=False)
- priority = ProjectRelatedField(slug_field="name")
- severity = ProjectRelatedField(slug_field="name")
- type = ProjectRelatedField(slug_field="name")
- milestone = ProjectRelatedField(slug_field="name", required=False)
- votes = serializers.SerializerMethodField("get_votes")
- modified_date = serializers.DateTimeField(required=False)
+class EpicRelatedUserStoryExportSerializer(RelatedExportSerializer):
+ user_story = SlugRelatedField(slug_field="ref")
+ order = Field()
- class Meta:
- model = issues_models.Issue
- exclude = ('id', 'project')
+
+class EpicExportSerializer(CustomAttributesValuesExportSerializerMixin,
+ HistoryExportSerializerMixin,
+ AttachmentExportSerializerMixin,
+ WatcheableObjectLightSerializerMixin,
+ RelatedExportSerializer):
+ ref = Field()
+ owner = UserRelatedField()
+ status = SlugRelatedField(slug_field="name")
+ epics_order = Field()
+ created_date = DateTimeField()
+ modified_date = DateTimeField()
+ subject = Field()
+ description = Field()
+ color = Field()
+ assigned_to = UserRelatedField()
+ client_requirement = Field()
+ team_requirement = Field()
+ version = Field()
+ blocked_note = Field()
+ is_blocked = Field()
+ tags = Field()
+ related_user_stories = MethodField()
+
+ def get_related_user_stories(self, obj):
+ return EpicRelatedUserStoryExportSerializer(obj.relateduserstory_set.all(), many=True).data
+
+ def custom_attributes_queryset(self, project):
+ if project.id not in _custom_epics_attributes_cache:
+ _custom_epics_attributes_cache[project.id] = list(
+ project.userstorycustomattributes.all().values('id', 'name')
+ )
+ return _custom_epics_attributes_cache[project.id]
+
+
+class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin,
+ HistoryExportSerializerMixin,
+ AttachmentExportSerializerMixin,
+ WatcheableObjectLightSerializerMixin,
+ RelatedExportSerializer):
+ owner = UserRelatedField()
+ status = SlugRelatedField(slug_field="name")
+ assigned_to = UserRelatedField()
+ priority = SlugRelatedField(slug_field="name")
+ severity = SlugRelatedField(slug_field="name")
+ type = SlugRelatedField(slug_field="name")
+ milestone = SlugRelatedField(slug_field="name")
+ votes = MethodField("get_votes")
+ modified_date = DateTimeField()
+ created_date = DateTimeField()
+ finished_date = DateTimeField()
+
+ ref = Field()
+ subject = Field()
+ description = Field()
+ external_reference = Field()
+ version = Field()
+ blocked_note = Field()
+ is_blocked = Field()
+ tags = Field()
def get_votes(self, obj):
return [x.email for x in votes_service.get_voters(obj)]
@@ -297,65 +330,99 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History
return _custom_issues_attributes_cache[project.id]
-class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
- WatcheableObjectModelSerializerMixin):
- owner = UserRelatedField(required=False)
- last_modifier = UserRelatedField(required=False)
- modified_date = serializers.DateTimeField(required=False)
-
- class Meta:
- model = wiki_models.WikiPage
- exclude = ('id', 'project')
+class WikiPageExportSerializer(HistoryExportSerializerMixin,
+ AttachmentExportSerializerMixin,
+ WatcheableObjectLightSerializerMixin,
+ RelatedExportSerializer):
+ slug = Field()
+ owner = UserRelatedField()
+ last_modifier = UserRelatedField()
+ modified_date = DateTimeField()
+ created_date = DateTimeField()
+ content = Field()
+ version = Field()
-class WikiLinkExportSerializer(serializers.ModelSerializer):
- class Meta:
- model = wiki_models.WikiLink
- exclude = ('id', 'project')
+class WikiLinkExportSerializer(RelatedExportSerializer):
+ title = Field()
+ href = Field()
+ order = Field()
-
-class TimelineExportSerializer(serializers.ModelSerializer):
+class TimelineExportSerializer(RelatedExportSerializer):
data = TimelineDataField()
data_content_type = ContentTypeField()
- class Meta:
- model = timeline_models.Timeline
- exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')
+ event_type = Field()
+ created = DateTimeField()
-class ProjectExportSerializer(WatcheableObjectModelSerializerMixin):
- logo = FileField(required=False)
- anon_permissions = PgArrayField(required=False)
- public_permissions = PgArrayField(required=False)
- modified_date = serializers.DateTimeField(required=False)
- roles = RoleExportSerializer(many=True, required=False)
- owner = UserRelatedField(required=False)
- memberships = MembershipExportSerializer(many=True, required=False)
- points = PointsExportSerializer(many=True, required=False)
- us_statuses = UserStoryStatusExportSerializer(many=True, required=False)
- task_statuses = TaskStatusExportSerializer(many=True, required=False)
- issue_types = IssueTypeExportSerializer(many=True, required=False)
- issue_statuses = IssueStatusExportSerializer(many=True, required=False)
- priorities = PriorityExportSerializer(many=True, required=False)
- severities = SeverityExportSerializer(many=True, required=False)
- tags_colors = JsonField(required=False)
- default_points = serializers.SlugRelatedField(slug_field="name", required=False)
- default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
- default_task_status = serializers.SlugRelatedField(slug_field="name", required=False)
- default_priority = serializers.SlugRelatedField(slug_field="name", required=False)
- default_severity = serializers.SlugRelatedField(slug_field="name", required=False)
- default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False)
- default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False)
- userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False)
- taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False)
- issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False)
- user_stories = UserStoryExportSerializer(many=True, required=False)
- tasks = TaskExportSerializer(many=True, required=False)
- milestones = MilestoneExportSerializer(many=True, required=False)
- issues = IssueExportSerializer(many=True, required=False)
- wiki_links = WikiLinkExportSerializer(many=True, required=False)
- wiki_pages = WikiPageExportSerializer(many=True, required=False)
-
- class Meta:
- model = projects_models.Project
- exclude = ('id', 'creation_template', 'members')
+class ProjectExportSerializer(WatcheableObjectLightSerializerMixin):
+ name = Field()
+ slug = Field()
+ description = Field()
+ created_date = DateTimeField()
+ logo = FileField()
+ total_milestones = Field()
+ total_story_points = Field()
+ is_epics_activated = Field()
+ is_backlog_activated = Field()
+ is_kanban_activated = Field()
+ is_wiki_activated = Field()
+ is_issues_activated = Field()
+ videoconferences = Field()
+ videoconferences_extra_data = Field()
+ creation_template = SlugRelatedField(slug_field="slug")
+ is_private = Field()
+ is_featured = Field()
+ is_looking_for_people = Field()
+ looking_for_people_note = Field()
+ epics_csv_uuid = Field()
+ userstories_csv_uuid = Field()
+ tasks_csv_uuid = Field()
+ issues_csv_uuid = Field()
+ transfer_token = Field()
+ blocked_code = Field()
+ totals_updated_datetime = DateTimeField()
+ total_fans = Field()
+ total_fans_last_week = Field()
+ total_fans_last_month = Field()
+ total_fans_last_year = Field()
+ total_activity = Field()
+ total_activity_last_week = Field()
+ total_activity_last_month = Field()
+ total_activity_last_year = Field()
+ anon_permissions = Field()
+ public_permissions = Field()
+ modified_date = DateTimeField()
+ roles = RoleExportSerializer(many=True)
+ owner = UserRelatedField()
+ memberships = MembershipExportSerializer(many=True)
+ points = PointsExportSerializer(many=True)
+ epic_statuses = EpicStatusExportSerializer(many=True)
+ us_statuses = UserStoryStatusExportSerializer(many=True)
+ task_statuses = TaskStatusExportSerializer(many=True)
+ issue_types = IssueTypeExportSerializer(many=True)
+ issue_statuses = IssueStatusExportSerializer(many=True)
+ priorities = PriorityExportSerializer(many=True)
+ severities = SeverityExportSerializer(many=True)
+ tags_colors = Field()
+ default_points = SlugRelatedField(slug_field="name")
+ default_epic_status = SlugRelatedField(slug_field="name")
+ default_us_status = SlugRelatedField(slug_field="name")
+ default_task_status = SlugRelatedField(slug_field="name")
+ default_priority = SlugRelatedField(slug_field="name")
+ default_severity = SlugRelatedField(slug_field="name")
+ default_issue_status = SlugRelatedField(slug_field="name")
+ default_issue_type = SlugRelatedField(slug_field="name")
+ epiccustomattributes = EpicCustomAttributesExportSerializer(many=True)
+ userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True)
+ taskcustomattributes = TaskCustomAttributeExportSerializer(many=True)
+ issuecustomattributes = IssueCustomAttributeExportSerializer(many=True)
+ epics = EpicExportSerializer(many=True)
+ user_stories = UserStoryExportSerializer(many=True)
+ tasks = TaskExportSerializer(many=True)
+ milestones = MilestoneExportSerializer(many=True)
+ issues = IssueExportSerializer(many=True)
+ wiki_links = WikiLinkExportSerializer(many=True)
+ wiki_pages = WikiPageExportSerializer(many=True)
+ tags = Field()
diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py
index 923647a7..cb757dd0 100644
--- a/taiga/export_import/services/render.py
+++ b/taiga/export_import/services/render.py
@@ -19,49 +19,48 @@
# This makes all code that import services works and
# is not the baddest practice ;)
-import base64
import gc
-import os
-
-from django.core.files.storage import default_storage
from taiga.base.utils import json
+from taiga.base.fields import MethodField
from taiga.timeline.service import get_project_timeline
from taiga.base.api.fields import get_component
from .. import serializers
-def render_project(project, outfile, chunk_size = 8190):
+def render_project(project, outfile, chunk_size=8190):
serializer = serializers.ProjectExportSerializer(project)
outfile.write(b'{\n')
first_field = True
- for field_name in serializer.fields.keys():
+ for field_name in serializer._field_map.keys():
# Avoid writing "," in the last element
if not first_field:
outfile.write(b",\n")
else:
first_field = False
- field = serializer.fields.get(field_name)
- field.initialize(parent=serializer, field_name=field_name)
+ field = serializer._field_map.get(field_name)
+ # field.initialize(parent=serializer, field_name=field_name)
# These four "special" fields hava attachments so we use them in a special way
- if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]:
+ if field_name in ["wiki_pages", "user_stories", "tasks", "issues", "epics"]:
value = get_component(project, field_name)
if field_name != "wiki_pages":
- value = value.select_related('owner', 'status', 'milestone', 'project', 'assigned_to', 'custom_attributes_values')
+ value = value.select_related('owner', 'status',
+ 'project', 'assigned_to',
+ 'custom_attributes_values')
+
+ if field_name in ["user_stories", "tasks", "issues"]:
+ value = value.select_related('milestone')
+
if field_name == "issues":
value = value.select_related('severity', 'priority', 'type')
value = value.prefetch_related('history_entry', 'attachments')
outfile.write('"{}": [\n'.format(field_name).encode())
- attachments_field = field.fields.pop("attachments", None)
- if attachments_field:
- attachments_field.initialize(parent=field, field_name="attachments")
-
first_item = True
for item in value.iterator():
# Avoid writing "," in the last element
@@ -70,47 +69,18 @@ def render_project(project, outfile, chunk_size = 8190):
else:
first_item = False
-
- dumped_value = json.dumps(field.to_native(item))
- writing_value = dumped_value[:-1]+ ',\n "attachments": [\n'
- outfile.write(writing_value.encode())
-
- first_attachment = True
- for attachment in item.attachments.iterator():
- # Avoid writing "," in the last element
- if not first_attachment:
- outfile.write(b",\n")
- else:
- first_attachment = False
-
- # Write all the data expect the serialized file
- attachment_serializer = serializers.AttachmentExportSerializer(instance=attachment)
- attached_file_serializer = attachment_serializer.fields.pop("attached_file")
- dumped_value = json.dumps(attachment_serializer.data)
- dumped_value = dumped_value[:-1] + ',\n "attached_file":{\n "data":"'
- outfile.write(dumped_value.encode())
-
- # We write the attached_files by chunks so the memory used is not increased
- attachment_file = attachment.attached_file
- if default_storage.exists(attachment_file.name):
- with default_storage.open(attachment_file.name) as f:
- while True:
- bin_data = f.read(chunk_size)
- if not bin_data:
- break
-
- b64_data = base64.b64encode(bin_data)
- outfile.write(b64_data)
-
- outfile.write('", \n "name":"{}"}}\n}}'.format(
- os.path.basename(attachment_file.name)).encode())
-
- outfile.write(b']}')
+ field.many = False
+ dumped_value = json.dumps(field.to_value(item))
+ outfile.write(dumped_value.encode())
outfile.flush()
gc.collect()
outfile.write(b']')
else:
- value = field.field_to_native(project, field_name)
+ if isinstance(field, MethodField):
+ value = field.as_getter(field_name, serializers.ProjectExportSerializer)(serializer, project)
+ else:
+ attr = getattr(project, field_name)
+ value = field.to_value(attr)
outfile.write('"{}": {}'.format(field_name, json.dumps(value)).encode())
# Generate the timeline
@@ -127,4 +97,3 @@ def render_project(project, outfile, chunk_size = 8190):
outfile.write(dumped_value.encode())
outfile.write(b']}\n')
-
diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py
index 5d71c445..e28353bc 100644
--- a/taiga/export_import/services/store.py
+++ b/taiga/export_import/services/store.py
@@ -39,7 +39,7 @@ from taiga.timeline.service import build_project_namespace
from taiga.users import services as users_service
from .. import exceptions as err
-from .. import serializers
+from .. import validators
########################################################################
@@ -80,23 +80,29 @@ def store_project(data):
excluded_fields = [
"default_points", "default_us_status", "default_task_status",
"default_priority", "default_severity", "default_issue_status",
- "default_issue_type", "memberships", "points", "us_statuses",
- "task_statuses", "issue_statuses", "priorities", "severities",
- "issue_types", "userstorycustomattributes", "taskcustomattributes",
- "issuecustomattributes", "roles", "milestones", "wiki_pages",
- "wiki_links", "notify_policies", "user_stories", "issues", "tasks",
+ "default_issue_type", "default_epic_status",
+ "memberships", "points",
+ "epic_statuses", "us_statuses", "task_statuses", "issue_statuses",
+ "priorities", "severities",
+ "issue_types",
+ "epiccustomattributes", "userstorycustomattributes",
+ "taskcustomattributes", "issuecustomattributes",
+ "roles", "milestones",
+ "wiki_pages", "wiki_links",
+ "notify_policies",
+ "epics", "user_stories", "issues", "tasks",
"is_featured"
]
if key not in excluded_fields:
project_data[key] = value
- serialized = serializers.ProjectExportSerializer(data=project_data)
- if serialized.is_valid():
- serialized.object._importing = True
- serialized.object.save()
- serialized.save_watchers()
- return serialized
- add_errors("project", serialized.errors)
+ validator = validators.ProjectExportValidator(data=project_data)
+ if validator.is_valid():
+ validator.object._importing = True
+ validator.object.save()
+ validator.save_watchers()
+ return validator
+ add_errors("project", validator.errors)
return None
@@ -133,54 +139,55 @@ def _store_custom_attributes_values(obj, data_values, obj_field, serializer_clas
def _store_attachment(project, obj, attachment):
- serialized = serializers.AttachmentExportSerializer(data=attachment)
- if serialized.is_valid():
- serialized.object.content_type = ContentType.objects.get_for_model(obj.__class__)
- serialized.object.object_id = obj.id
- serialized.object.project = project
- if serialized.object.owner is None:
- serialized.object.owner = serialized.object.project.owner
- serialized.object._importing = True
- serialized.object.size = serialized.object.attached_file.size
- serialized.object.name = os.path.basename(serialized.object.attached_file.name)
- serialized.save()
- return serialized
- add_errors("attachments", serialized.errors)
- return serialized
+ validator = validators.AttachmentExportValidator(data=attachment)
+ if validator.is_valid():
+ validator.object.content_type = ContentType.objects.get_for_model(obj.__class__)
+ validator.object.object_id = obj.id
+ validator.object.project = project
+ if validator.object.owner is None:
+ validator.object.owner = validator.object.project.owner
+ validator.object._importing = True
+ validator.object.size = validator.object.attached_file.size
+ validator.object.name = os.path.basename(validator.object.attached_file.name)
+ validator.save()
+ return validator
+ add_errors("attachments", validator.errors)
+ return validator
def _store_history(project, obj, history):
- serialized = serializers.HistoryExportSerializer(data=history, context={"project": project})
- if serialized.is_valid():
- serialized.object.key = make_key_from_model_object(obj)
- if serialized.object.diff is None:
- serialized.object.diff = []
- serialized.object._importing = True
- serialized.save()
- return serialized
- add_errors("history", serialized.errors)
- return serialized
+ validator = validators.HistoryExportValidator(data=history, context={"project": project})
+ if validator.is_valid():
+ validator.object.key = make_key_from_model_object(obj)
+ if validator.object.diff is None:
+ validator.object.diff = []
+ validator.object.project_id = project.id
+ validator.object._importing = True
+ validator.save()
+ return validator
+ add_errors("history", validator.errors)
+ return validator
## ROLES
def _store_role(project, role):
- serialized = serializers.RoleExportSerializer(data=role)
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object._importing = True
- serialized.save()
- return serialized
- add_errors("roles", serialized.errors)
+ validator = validators.RoleExportValidator(data=role)
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object._importing = True
+ validator.save()
+ return validator
+ add_errors("roles", validator.errors)
return None
def store_roles(project, data):
results = []
for role in data.get("roles", []):
- serialized = _store_role(project, role)
- if serialized:
- results.append(serialized)
+ validator = _store_role(project, role)
+ if validator:
+ results.append(validator)
return results
@@ -188,17 +195,17 @@ def store_roles(project, data):
## MEMGERSHIPS
def _store_membership(project, membership):
- serialized = serializers.MembershipExportSerializer(data=membership, context={"project": project})
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object._importing = True
- serialized.object.token = str(uuid.uuid1())
- serialized.object.user = find_invited_user(serialized.object.email,
- default=serialized.object.user)
- serialized.save()
- return serialized
+ validator = validators.MembershipExportValidator(data=membership, context={"project": project})
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object._importing = True
+ validator.object.token = str(uuid.uuid1())
+ validator.object.user = find_invited_user(validator.object.email,
+ default=validator.object.user)
+ validator.save()
+ return validator
- add_errors("memberships", serialized.errors)
+ add_errors("memberships", validator.errors)
return None
@@ -212,13 +219,14 @@ def store_memberships(project, data):
## PROJECT ATTRIBUTES
def _store_project_attribute_value(project, data, field, serializer):
- serialized = serializer(data=data)
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object._importing = True
- serialized.save()
- return serialized.object
- add_errors(field, serialized.errors)
+ validator = serializer(data=data)
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object._importing = True
+ validator.save()
+ return validator.object
+
+ add_errors(field, validator.errors)
return None
@@ -238,10 +246,10 @@ def store_default_project_attributes_values(project, data):
else:
value = related.all().first()
setattr(project, field, value)
-
helper(project, "default_points", project.points, data)
helper(project, "default_issue_type", project.issue_types, data)
helper(project, "default_issue_status", project.issue_statuses, data)
+ helper(project, "default_epic_status", project.epic_statuses, data)
helper(project, "default_us_status", project.us_statuses, data)
helper(project, "default_task_status", project.task_statuses, data)
helper(project, "default_priority", project.priorities, data)
@@ -253,13 +261,13 @@ def store_default_project_attributes_values(project, data):
## CUSTOM ATTRIBUTES
def _store_custom_attribute(project, data, field, serializer):
- serialized = serializer(data=data)
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object._importing = True
- serialized.save()
- return serialized.object
- add_errors(field, serialized.errors)
+ validator = serializer(data=data)
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object._importing = True
+ validator.save()
+ return validator.object
+ add_errors(field, validator.errors)
return None
@@ -273,19 +281,19 @@ def store_custom_attributes(project, data, field, serializer):
## MILESTONE
def store_milestone(project, milestone):
- serialized = serializers.MilestoneExportSerializer(data=milestone, project=project)
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object._importing = True
- serialized.save()
- serialized.save_watchers()
+ validator = validators.MilestoneExportValidator(data=milestone, project=project)
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object._importing = True
+ validator.save()
+ validator.save_watchers()
for task_without_us in milestone.get("tasks_without_us", []):
task_without_us["user_story"] = None
store_task(project, task_without_us)
- return serialized
+ return validator
- add_errors("milestones", serialized.errors)
+ add_errors("milestones", validator.errors)
return None
@@ -300,73 +308,78 @@ def store_milestones(project, data):
## USER STORIES
def _store_role_point(project, us, role_point):
- serialized = serializers.RolePointsExportSerializer(data=role_point, context={"project": project})
- if serialized.is_valid():
+ validator = validators.RolePointsExportValidator(data=role_point, context={"project": project})
+ if validator.is_valid():
try:
- existing_role_point = us.role_points.get(role=serialized.object.role)
- existing_role_point.points = serialized.object.points
+ existing_role_point = us.role_points.get(role=validator.object.role)
+ existing_role_point.points = validator.object.points
existing_role_point.save()
return existing_role_point
except RolePoints.DoesNotExist:
- serialized.object.user_story = us
- serialized.save()
- return serialized.object
+ validator.object.user_story = us
+ validator.save()
+ return validator.object
- add_errors("role_points", serialized.errors)
+ add_errors("role_points", validator.errors)
return None
+
def store_user_story(project, data):
if "status" not in data and project.default_us_status:
data["status"] = project.default_us_status.name
us_data = {key: value for key, value in data.items() if key not in
- ["role_points", "custom_attributes_values"]}
- serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project})
+ ["role_points", "custom_attributes_values"]}
- if serialized.is_valid():
- serialized.object.project = project
- if serialized.object.owner is None:
- serialized.object.owner = serialized.object.project.owner
- serialized.object._importing = True
- serialized.object._not_notify = True
+ validator = validators.UserStoryExportValidator(data=us_data, context={"project": project})
- serialized.save()
- serialized.save_watchers()
+ if validator.is_valid():
+ validator.object.project = project
+ if validator.object.owner is None:
+ validator.object.owner = validator.object.project.owner
+ validator.object._importing = True
+ validator.object._not_notify = True
- if serialized.object.ref:
+ validator.save()
+ validator.save_watchers()
+
+ if validator.object.ref:
sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name):
seq.create(sequence_name)
- seq.set_max(sequence_name, serialized.object.ref)
+ seq.set_max(sequence_name, validator.object.ref)
else:
- serialized.object.ref, _ = refs.make_reference(serialized.object, project)
- serialized.object.save()
+ validator.object.ref, _ = refs.make_reference(validator.object, project)
+ validator.object.save()
for us_attachment in data.get("attachments", []):
- _store_attachment(project, serialized.object, us_attachment)
+ _store_attachment(project, validator.object, us_attachment)
for role_point in data.get("role_points", []):
- _store_role_point(project, serialized.object, role_point)
+ _store_role_point(project, validator.object, role_point)
history_entries = data.get("history", [])
for history in history_entries:
- _store_history(project, serialized.object, history)
+ _store_history(project, validator.object, history)
if not history_entries:
- take_snapshot(serialized.object, user=serialized.object.owner)
+ take_snapshot(validator.object, user=validator.object.owner)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
- custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name')
- custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
- custom_attributes, custom_attributes_values)
- _store_custom_attributes_values(serialized.object, custom_attributes_values,
- "user_story", serializers.UserStoryCustomAttributesValuesExportSerializer)
+ custom_attributes = validator.object.project.userstorycustomattributes.all().values('id', 'name')
+ custom_attributes_values = \
+ _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
+ custom_attributes_values)
- return serialized
+ _store_custom_attributes_values(validator.object, custom_attributes_values,
+ "user_story",
+ validators.UserStoryCustomAttributesValuesExportValidator)
- add_errors("user_stories", serialized.errors)
+ return validator
+
+ add_errors("user_stories", validator.errors)
return None
@@ -378,53 +391,131 @@ def store_user_stories(project, data):
return results
+## EPICS
+
+def _store_epic_related_user_story(project, epic, related_user_story):
+ validator = validators.EpicRelatedUserStoryExportValidator(data=related_user_story,
+ context={"project": project})
+ if validator.is_valid():
+ validator.object.epic = epic
+ validator.object.save()
+ return validator.object
+
+ add_errors("epic_related_user_stories", validator.errors)
+ return None
+
+
+def store_epic(project, data):
+ if "status" not in data and project.default_epic_status:
+ data["status"] = project.default_epic_status.name
+
+ validator = validators.EpicExportValidator(data=data, context={"project": project})
+ if validator.is_valid():
+ validator.object.project = project
+ if validator.object.owner is None:
+ validator.object.owner = validator.object.project.owner
+ validator.object._importing = True
+ validator.object._not_notify = True
+
+ validator.save()
+ validator.save_watchers()
+
+ if validator.object.ref:
+ sequence_name = refs.make_sequence_name(project)
+ if not seq.exists(sequence_name):
+ seq.create(sequence_name)
+ seq.set_max(sequence_name, validator.object.ref)
+ else:
+ validator.object.ref, _ = refs.make_reference(validator.object, project)
+ validator.object.save()
+
+ for epic_attachment in data.get("attachments", []):
+ _store_attachment(project, validator.object, epic_attachment)
+
+ for related_user_story in data.get("related_user_stories", []):
+ _store_epic_related_user_story(project, validator.object, related_user_story)
+
+ history_entries = data.get("history", [])
+ for history in history_entries:
+ _store_history(project, validator.object, history)
+
+ if not history_entries:
+ take_snapshot(validator.object, user=validator.object.owner)
+
+ custom_attributes_values = data.get("custom_attributes_values", None)
+ if custom_attributes_values:
+ custom_attributes = validator.object.project.epiccustomattributes.all().values('id', 'name')
+ custom_attributes_values = \
+ _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
+ custom_attributes_values)
+ _store_custom_attributes_values(validator.object, custom_attributes_values,
+ "epic",
+ validators.EpicCustomAttributesValuesExportValidator)
+
+ return validator
+
+ add_errors("epics", validator.errors)
+ return None
+
+
+def store_epics(project, data):
+ results = []
+ for epic in data.get("epics", []):
+ epic = store_epic(project, epic)
+ results.append(epic)
+ return results
+
+
## TASKS
def store_task(project, data):
if "status" not in data and project.default_task_status:
data["status"] = project.default_task_status.name
- serialized = serializers.TaskExportSerializer(data=data, context={"project": project})
- if serialized.is_valid():
- serialized.object.project = project
- if serialized.object.owner is None:
- serialized.object.owner = serialized.object.project.owner
- serialized.object._importing = True
- serialized.object._not_notify = True
+ validator = validators.TaskExportValidator(data=data, context={"project": project})
+ if validator.is_valid():
+ validator.object.project = project
+ if validator.object.owner is None:
+ validator.object.owner = validator.object.project.owner
+ validator.object._importing = True
+ validator.object._not_notify = True
- serialized.save()
- serialized.save_watchers()
+ validator.save()
+ validator.save_watchers()
- if serialized.object.ref:
+ if validator.object.ref:
sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name):
seq.create(sequence_name)
- seq.set_max(sequence_name, serialized.object.ref)
+ seq.set_max(sequence_name, validator.object.ref)
else:
- serialized.object.ref, _ = refs.make_reference(serialized.object, project)
- serialized.object.save()
+ validator.object.ref, _ = refs.make_reference(validator.object, project)
+ validator.object.save()
for task_attachment in data.get("attachments", []):
- _store_attachment(project, serialized.object, task_attachment)
+ _store_attachment(project, validator.object, task_attachment)
history_entries = data.get("history", [])
for history in history_entries:
- _store_history(project, serialized.object, history)
+ _store_history(project, validator.object, history)
if not history_entries:
- take_snapshot(serialized.object, user=serialized.object.owner)
+ take_snapshot(validator.object, user=validator.object.owner)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
- custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name')
- custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
- custom_attributes, custom_attributes_values)
- _store_custom_attributes_values(serialized.object, custom_attributes_values,
- "task", serializers.TaskCustomAttributesValuesExportSerializer)
+ custom_attributes = validator.object.project.taskcustomattributes.all().values('id', 'name')
+ custom_attributes_values = \
+ _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
+ custom_attributes_values)
- return serialized
+ _store_custom_attributes_values(validator.object, custom_attributes_values,
+ "task",
+ validators.TaskCustomAttributesValuesExportValidator)
- add_errors("tasks", serialized.errors)
+ return validator
+
+ add_errors("tasks", validator.errors)
return None
@@ -439,7 +530,7 @@ def store_tasks(project, data):
## ISSUES
def store_issue(project, data):
- serialized = serializers.IssueExportSerializer(data=data, context={"project": project})
+ validator = validators.IssueExportValidator(data=data, context={"project": project})
if "type" not in data and project.default_issue_type:
data["type"] = project.default_issue_type.name
@@ -453,46 +544,48 @@ def store_issue(project, data):
if "severity" not in data and project.default_severity:
data["severity"] = project.default_severity.name
- if serialized.is_valid():
- serialized.object.project = project
- if serialized.object.owner is None:
- serialized.object.owner = serialized.object.project.owner
- serialized.object._importing = True
- serialized.object._not_notify = True
+ if validator.is_valid():
+ validator.object.project = project
+ if validator.object.owner is None:
+ validator.object.owner = validator.object.project.owner
+ validator.object._importing = True
+ validator.object._not_notify = True
- serialized.save()
- serialized.save_watchers()
+ validator.save()
+ validator.save_watchers()
- if serialized.object.ref:
+ if validator.object.ref:
sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name):
seq.create(sequence_name)
- seq.set_max(sequence_name, serialized.object.ref)
+ seq.set_max(sequence_name, validator.object.ref)
else:
- serialized.object.ref, _ = refs.make_reference(serialized.object, project)
- serialized.object.save()
+ validator.object.ref, _ = refs.make_reference(validator.object, project)
+ validator.object.save()
for attachment in data.get("attachments", []):
- _store_attachment(project, serialized.object, attachment)
+ _store_attachment(project, validator.object, attachment)
history_entries = data.get("history", [])
for history in history_entries:
- _store_history(project, serialized.object, history)
+ _store_history(project, validator.object, history)
if not history_entries:
- take_snapshot(serialized.object, user=serialized.object.owner)
+ take_snapshot(validator.object, user=validator.object.owner)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
- custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name')
- custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
- custom_attributes, custom_attributes_values)
- _store_custom_attributes_values(serialized.object, custom_attributes_values,
- "issue", serializers.IssueCustomAttributesValuesExportSerializer)
+ custom_attributes = validator.object.project.issuecustomattributes.all().values('id', 'name')
+ custom_attributes_values = \
+ _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
+ custom_attributes_values)
+ _store_custom_attributes_values(validator.object, custom_attributes_values,
+ "issue",
+ validators.IssueCustomAttributesValuesExportValidator)
- return serialized
+ return validator
- add_errors("issues", serialized.errors)
+ add_errors("issues", validator.errors)
return None
@@ -507,29 +600,29 @@ def store_issues(project, data):
def store_wiki_page(project, wiki_page):
wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", "")))
- serialized = serializers.WikiPageExportSerializer(data=wiki_page)
- if serialized.is_valid():
- serialized.object.project = project
- if serialized.object.owner is None:
- serialized.object.owner = serialized.object.project.owner
- serialized.object._importing = True
- serialized.object._not_notify = True
- serialized.save()
- serialized.save_watchers()
+ validator = validators.WikiPageExportValidator(data=wiki_page)
+ if validator.is_valid():
+ validator.object.project = project
+ if validator.object.owner is None:
+ validator.object.owner = validator.object.project.owner
+ validator.object._importing = True
+ validator.object._not_notify = True
+ validator.save()
+ validator.save_watchers()
for attachment in wiki_page.get("attachments", []):
- _store_attachment(project, serialized.object, attachment)
+ _store_attachment(project, validator.object, attachment)
history_entries = wiki_page.get("history", [])
for history in history_entries:
- _store_history(project, serialized.object, history)
+ _store_history(project, validator.object, history)
if not history_entries:
- take_snapshot(serialized.object, user=serialized.object.owner)
+ take_snapshot(validator.object, user=validator.object.owner)
- return serialized
+ return validator
- add_errors("wiki_pages", serialized.errors)
+ add_errors("wiki_pages", validator.errors)
return None
@@ -543,14 +636,14 @@ def store_wiki_pages(project, data):
## WIKI LINKS
def store_wiki_link(project, wiki_link):
- serialized = serializers.WikiLinkExportSerializer(data=wiki_link)
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object._importing = True
- serialized.save()
- return serialized
+ validator = validators.WikiLinkExportValidator(data=wiki_link)
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object._importing = True
+ validator.save()
+ return validator
- add_errors("wiki_links", serialized.errors)
+ add_errors("wiki_links", validator.errors)
return None
@@ -572,17 +665,17 @@ def store_tags_colors(project, data):
## TIMELINE
def _store_timeline_entry(project, timeline):
- serialized = serializers.TimelineExportSerializer(data=timeline, context={"project": project})
- if serialized.is_valid():
- serialized.object.project = project
- serialized.object.namespace = build_project_namespace(project)
- serialized.object.object_id = project.id
- serialized.object.content_type = ContentType.objects.get_for_model(project.__class__)
- serialized.object._importing = True
- serialized.save()
- return serialized
- add_errors("timeline", serialized.errors)
- return serialized
+ validator = validators.TimelineExportValidator(data=timeline, context={"project": project})
+ if validator.is_valid():
+ validator.object.project = project
+ validator.object.namespace = build_project_namespace(project)
+ validator.object.object_id = project.id
+ validator.object.content_type = ContentType.objects.get_for_model(project.__class__)
+ validator.object._importing = True
+ validator.save()
+ return validator
+ add_errors("timeline", validator.errors)
+ return validator
def store_timeline_entries(project, data):
@@ -604,8 +697,9 @@ def _validate_if_owner_have_enought_space_to_this_project(owner, data):
is_private = data.get("is_private", False)
total_memberships = len([m for m in data.get("memberships", [])
- if m.get("email", None) != data["owner"]])
- total_memberships = total_memberships + 1 # 1 is the owner
+ if m.get("email", None) != data["owner"]])
+
+ total_memberships = total_memberships + 1 # 1 is the owner
(enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
owner,
is_private,
@@ -617,13 +711,13 @@ def _validate_if_owner_have_enought_space_to_this_project(owner, data):
def _create_project_object(data):
# Create the project
- project_serialized = store_project(data)
+ project_validator = store_project(data)
- if not project_serialized:
+ if not project_validator:
errors = get_errors(clear=True)
raise err.TaigaImportError(_("error importing project data"), None, errors=errors)
- return project_serialized.object if project_serialized else None
+ return project_validator.object if project_validator else None
def _create_membership_for_project_owner(project):
@@ -651,16 +745,17 @@ def _populate_project_object(project, data):
# Create memberships
store_memberships(project, data)
_create_membership_for_project_owner(project)
- check_if_there_is_some_error(_("error importing memberships"), project)
+ check_if_there_is_some_error(_("error importing memberships"), project)
# Create project attributes values
- store_project_attributes_values(project, data, "us_statuses", serializers.UserStoryStatusExportSerializer)
- store_project_attributes_values(project, data, "points", serializers.PointsExportSerializer)
- store_project_attributes_values(project, data, "task_statuses", serializers.TaskStatusExportSerializer)
- store_project_attributes_values(project, data, "issue_types", serializers.IssueTypeExportSerializer)
- store_project_attributes_values(project, data, "issue_statuses", serializers.IssueStatusExportSerializer)
- store_project_attributes_values(project, data, "priorities", serializers.PriorityExportSerializer)
- store_project_attributes_values(project, data, "severities", serializers.SeverityExportSerializer)
+ store_project_attributes_values(project, data, "epic_statuses", validators.EpicStatusExportValidator)
+ store_project_attributes_values(project, data, "us_statuses", validators.UserStoryStatusExportValidator)
+ store_project_attributes_values(project, data, "points", validators.PointsExportValidator)
+ store_project_attributes_values(project, data, "task_statuses", validators.TaskStatusExportValidator)
+ store_project_attributes_values(project, data, "issue_types", validators.IssueTypeExportValidator)
+ store_project_attributes_values(project, data, "issue_statuses", validators.IssueStatusExportValidator)
+ store_project_attributes_values(project, data, "priorities", validators.PriorityExportValidator)
+ store_project_attributes_values(project, data, "severities", validators.SeverityExportValidator)
check_if_there_is_some_error(_("error importing lists of project attributes"), project)
# Create default values for project attributes
@@ -668,12 +763,14 @@ def _populate_project_object(project, data):
check_if_there_is_some_error(_("error importing default project attributes values"), project)
# Create custom attributes
+ store_custom_attributes(project, data, "epiccustomattributes",
+ validators.EpicCustomAttributeExportValidator)
store_custom_attributes(project, data, "userstorycustomattributes",
- serializers.UserStoryCustomAttributeExportSerializer)
+ validators.UserStoryCustomAttributeExportValidator)
store_custom_attributes(project, data, "taskcustomattributes",
- serializers.TaskCustomAttributeExportSerializer)
+ validators.TaskCustomAttributeExportValidator)
store_custom_attributes(project, data, "issuecustomattributes",
- serializers.IssueCustomAttributeExportSerializer)
+ validators.IssueCustomAttributeExportValidator)
check_if_there_is_some_error(_("error importing custom attributes"), project)
# Create milestones
@@ -688,6 +785,10 @@ def _populate_project_object(project, data):
store_user_stories(project, data)
check_if_there_is_some_error(_("error importing user stories"), project)
+ # Creat epics
+ store_epics(project, data)
+ check_if_there_is_some_error(_("error importing epics"), project)
+
# Createer tasks
store_tasks(project, data)
check_if_there_is_some_error(_("error importing tasks"), project)
diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py
index aa75c257..5acb08a2 100644
--- a/taiga/export_import/tasks.py
+++ b/taiga/export_import/tasks.py
@@ -46,13 +46,11 @@ def dump_project(self, user, project, dump_format):
try:
if dump_format == "gzip":
path = "exports/{}/{}-{}.json.gz".format(project.pk, project.slug, self.request.id)
- storage_path = default_storage.path(path)
- with default_storage.open(storage_path, mode="wb") as outfile:
+ with default_storage.open(path, mode="wb") as outfile:
services.render_project(project, gzip.GzipFile(fileobj=outfile))
else:
path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id)
- storage_path = default_storage.path(path)
- with default_storage.open(storage_path, mode="wb") as outfile:
+ with default_storage.open(path, mode="wb") as outfile:
services.render_project(project, outfile)
url = default_storage.url(path)
diff --git a/taiga/export_import/validators/__init__.py b/taiga/export_import/validators/__init__.py
new file mode 100644
index 00000000..0948ade0
--- /dev/null
+++ b/taiga/export_import/validators/__init__.py
@@ -0,0 +1,31 @@
+from .validators import PointsExportValidator
+from .validators import EpicStatusExportValidator
+from .validators import UserStoryStatusExportValidator
+from .validators import TaskStatusExportValidator
+from .validators import IssueStatusExportValidator
+from .validators import PriorityExportValidator
+from .validators import SeverityExportValidator
+from .validators import IssueTypeExportValidator
+from .validators import RoleExportValidator
+from .validators import EpicCustomAttributeExportValidator
+from .validators import UserStoryCustomAttributeExportValidator
+from .validators import TaskCustomAttributeExportValidator
+from .validators import IssueCustomAttributeExportValidator
+from .validators import BaseCustomAttributesValuesExportValidator
+from .validators import UserStoryCustomAttributesValuesExportValidator
+from .validators import TaskCustomAttributesValuesExportValidator
+from .validators import IssueCustomAttributesValuesExportValidator
+from .validators import MembershipExportValidator
+from .validators import RolePointsExportValidator
+from .validators import MilestoneExportValidator
+from .validators import TaskExportValidator
+from .validators import EpicRelatedUserStoryExportValidator
+from .validators import EpicExportValidator
+from .validators import UserStoryExportValidator
+from .validators import IssueExportValidator
+from .validators import WikiPageExportValidator
+from .validators import WikiLinkExportValidator
+from .validators import TimelineExportValidator
+from .validators import ProjectExportValidator
+from .mixins import AttachmentExportValidator
+from .mixins import HistoryExportValidator
diff --git a/taiga/export_import/validators/cache.py b/taiga/export_import/validators/cache.py
new file mode 100644
index 00000000..d82e943d
--- /dev/null
+++ b/taiga/export_import/validators/cache.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.users import models as users_models
+
+_cache_user_by_pk = {}
+_cache_user_by_email = {}
+_custom_tasks_attributes_cache = {}
+_custom_issues_attributes_cache = {}
+_custom_epics_attributes_cache = {}
+_custom_userstories_attributes_cache = {}
+
+
+def cached_get_user_by_pk(pk):
+ if pk not in _cache_user_by_pk:
+ try:
+ _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk)
+ except Exception:
+ _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk)
+ return _cache_user_by_pk[pk]
+
+def cached_get_user_by_email(email):
+ if email not in _cache_user_by_email:
+ try:
+ _cache_user_by_email[email] = users_models.User.objects.get(email=email)
+ except Exception:
+ _cache_user_by_email[email] = users_models.User.objects.get(email=email)
+ return _cache_user_by_email[email]
diff --git a/taiga/export_import/validators/fields.py b/taiga/export_import/validators/fields.py
new file mode 100644
index 00000000..e3d33c7a
--- /dev/null
+++ b/taiga/export_import/validators/fields.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import base64
+import copy
+
+from django.core.files.base import ContentFile
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.translation import ugettext as _
+from django.contrib.contenttypes.models import ContentType
+
+from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import JsonField
+from taiga.mdrender.service import render as mdrender
+from taiga.users import models as users_models
+
+from .cache import cached_get_user_by_email
+
+
+class FileField(serializers.WritableField):
+ read_only = False
+
+ def from_native(self, data):
+ if not data:
+ return None
+
+ decoded_data = b''
+ # The original file was encoded by chunks but we don't really know its
+ # length or if it was multiple of 3 so we must iterate over all those chunks
+ # decoding them one by one
+ for decoding_chunk in data['data'].split("="):
+ # When encoding to base64 3 bytes are transformed into 4 bytes and
+ # the extra space of the block is filled with =
+ # We must ensure that the decoding chunk has a length multiple of 4 so
+ # we restore the stripped '='s adding appending them until the chunk has
+ # a length multiple of 4
+ decoding_chunk += "=" * (-len(decoding_chunk) % 4)
+ decoded_data += base64.b64decode(decoding_chunk + "=")
+
+ return ContentFile(decoded_data, name=data['name'])
+
+
+class ContentTypeField(serializers.RelatedField):
+ read_only = False
+
+ def from_native(self, data):
+ try:
+ return ContentType.objects.get_by_natural_key(*data)
+ except Exception:
+ return None
+
+
+class RelatedNoneSafeField(serializers.RelatedField):
+ def field_from_native(self, data, files, field_name, into):
+ if self.read_only:
+ return
+
+ try:
+ if self.many:
+ try:
+ # Form data
+ value = data.getlist(field_name)
+ if value == [''] or value == []:
+ raise KeyError
+ except AttributeError:
+ # Non-form data
+ value = data[field_name]
+ else:
+ value = data[field_name]
+ except KeyError:
+ if self.partial:
+ return
+ value = self.get_default_value()
+
+ key = self.source or field_name
+ if value in self.null_values:
+ if self.required:
+ raise ValidationError(self.error_messages['required'])
+ into[key] = None
+ elif self.many:
+ into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None]
+ else:
+ into[key] = self.from_native(value)
+
+
+class UserRelatedField(RelatedNoneSafeField):
+ read_only = False
+
+ def from_native(self, data):
+ try:
+ return cached_get_user_by_email(data)
+ except users_models.User.DoesNotExist:
+ return None
+
+
+class UserPkField(serializers.RelatedField):
+ read_only = False
+
+ def from_native(self, data):
+ try:
+ user = cached_get_user_by_email(data)
+ return user.pk
+ except users_models.User.DoesNotExist:
+ return None
+
+
+class CommentField(serializers.WritableField):
+ read_only = False
+
+ def field_from_native(self, data, files, field_name, into):
+ super().field_from_native(data, files, field_name, into)
+ into["comment_html"] = mdrender(self.context['project'], data.get("comment", ""))
+
+
+class ProjectRelatedField(serializers.RelatedField):
+ read_only = False
+ null_values = (None, "")
+
+ def __init__(self, slug_field, *args, **kwargs):
+ self.slug_field = slug_field
+ super().__init__(*args, **kwargs)
+
+ def from_native(self, data):
+ try:
+ kwargs = {self.slug_field: data, "project": self.context['project']}
+ return self.queryset.get(**kwargs)
+ except ObjectDoesNotExist:
+ raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data)))
+
+
+class HistoryUserField(JsonField):
+ def from_native(self, data):
+ if data is None:
+ return {}
+
+ if len(data) < 2:
+ return {}
+
+ user = UserRelatedField().from_native(data[0])
+
+ if user:
+ pk = user.pk
+ else:
+ pk = None
+
+ return {"pk": pk, "name": data[1]}
+
+
+class HistoryValuesField(JsonField):
+ def from_native(self, data):
+ if data is None:
+ return []
+ if "users" in data:
+ data['users'] = list(map(UserPkField().from_native, data['users']))
+ return data
+
+
+class HistoryDiffField(JsonField):
+ def from_native(self, data):
+ if data is None:
+ return []
+
+ if "assigned_to" in data:
+ data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to']))
+ return data
+
+
+class TimelineDataField(serializers.WritableField):
+ read_only = False
+
+ def from_native(self, data):
+ new_data = copy.deepcopy(data)
+ try:
+ user = cached_get_user_by_email(new_data["user"]["email"])
+ new_data["user"]["id"] = user.id
+ del new_data["user"]["email"]
+ except users_models.User.DoesNotExist:
+ pass
+
+ return new_data
diff --git a/taiga/export_import/validators/mixins.py b/taiga/export_import/validators/mixins.py
new file mode 100644
index 00000000..d07334b6
--- /dev/null
+++ b/taiga/export_import/validators/mixins.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.contrib.auth import get_user_model
+from django.core.exceptions import ObjectDoesNotExist
+from django.contrib.contenttypes.models import ContentType
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.projects.history import models as history_models
+from taiga.projects.attachments import models as attachments_models
+from taiga.projects.notifications import services as notifications_services
+from taiga.projects.history import services as history_service
+
+from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField,
+ JsonField, HistoryValuesField, CommentField, FileField)
+
+
+class HistoryExportValidator(validators.ModelValidator):
+ user = HistoryUserField()
+ diff = HistoryDiffField(required=False)
+ snapshot = JsonField(required=False)
+ values = HistoryValuesField(required=False)
+ comment = CommentField(required=False)
+ delete_comment_date = serializers.DateTimeField(required=False)
+ delete_comment_user = HistoryUserField(required=False)
+
+ class Meta:
+ model = history_models.HistoryEntry
+ exclude = ("id", "comment_html", "key", "project")
+
+
+class AttachmentExportValidator(validators.ModelValidator):
+ owner = UserRelatedField(required=False)
+ attached_file = FileField()
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = attachments_models.Attachment
+ exclude = ('id', 'content_type', 'object_id', 'project')
+
+
+class WatcheableObjectModelValidatorMixin(validators.ModelValidator):
+ watchers = UserRelatedField(many=True, required=False)
+
+ def __init__(self, *args, **kwargs):
+ self._watchers_field = self.base_fields.pop("watchers", None)
+ super(WatcheableObjectModelValidatorMixin, self).__init__(*args, **kwargs)
+
+ """
+ watchers is not a field from the model so we need to do some magic to make it work like a normal field
+ It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances
+ """
+
+ def restore_object(self, attrs, instance=None):
+ self.fields.pop("watchers", None)
+ instance = super(WatcheableObjectModelValidatorMixin, self).restore_object(attrs, instance)
+ self._watchers = self.init_data.get("watchers", [])
+ return instance
+
+ def save_watchers(self):
+ new_watcher_emails = set(self._watchers)
+ old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True))
+ adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails))
+ removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails))
+
+ User = get_user_model()
+ adding_users = User.objects.filter(email__in=adding_watcher_emails)
+ removing_users = User.objects.filter(email__in=removing_watcher_emails)
+
+ for user in adding_users:
+ notifications_services.add_watcher(self.object, user)
+
+ for user in removing_users:
+ notifications_services.remove_watcher(self.object, user)
+
+ self.object.watchers = [user.email for user in self.object.get_watchers()]
+
+ def to_native(self, obj):
+ ret = super(WatcheableObjectModelValidatorMixin, self).to_native(obj)
+ ret["watchers"] = [user.email for user in obj.get_watchers()]
+ return ret
diff --git a/taiga/export_import/validators/validators.py b/taiga/export_import/validators/validators.py
new file mode 100644
index 00000000..c821b531
--- /dev/null
+++ b/taiga/export_import/validators/validators.py
@@ -0,0 +1,402 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.utils.translation import ugettext as _
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.fields import JsonField, PgArrayField
+from taiga.base.exceptions import ValidationError
+
+from taiga.projects import models as projects_models
+from taiga.projects.custom_attributes import models as custom_attributes_models
+from taiga.projects.epics import models as epics_models
+from taiga.projects.userstories import models as userstories_models
+from taiga.projects.tasks import models as tasks_models
+from taiga.projects.issues import models as issues_models
+from taiga.projects.milestones import models as milestones_models
+from taiga.projects.wiki import models as wiki_models
+from taiga.timeline import models as timeline_models
+from taiga.users import models as users_models
+
+from .fields import (FileField, UserRelatedField,
+ ProjectRelatedField,
+ TimelineDataField, ContentTypeField)
+from .mixins import WatcheableObjectModelValidatorMixin
+from .cache import (_custom_tasks_attributes_cache,
+ _custom_epics_attributes_cache,
+ _custom_userstories_attributes_cache,
+ _custom_issues_attributes_cache)
+
+
+class PointsExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.Points
+ exclude = ('id', 'project')
+
+
+class EpicStatusExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.EpicStatus
+ exclude = ('id', 'project')
+
+
+class UserStoryStatusExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.UserStoryStatus
+ exclude = ('id', 'project')
+
+
+class TaskStatusExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.TaskStatus
+ exclude = ('id', 'project')
+
+
+class IssueStatusExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.IssueStatus
+ exclude = ('id', 'project')
+
+
+class PriorityExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.Priority
+ exclude = ('id', 'project')
+
+
+class SeverityExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.Severity
+ exclude = ('id', 'project')
+
+
+class IssueTypeExportValidator(validators.ModelValidator):
+ class Meta:
+ model = projects_models.IssueType
+ exclude = ('id', 'project')
+
+
+class RoleExportValidator(validators.ModelValidator):
+ permissions = PgArrayField(required=False)
+
+ class Meta:
+ model = users_models.Role
+ exclude = ('id', 'project')
+
+
+class EpicCustomAttributeExportValidator(validators.ModelValidator):
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = custom_attributes_models.EpicCustomAttribute
+ exclude = ('id', 'project')
+
+
+class UserStoryCustomAttributeExportValidator(validators.ModelValidator):
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = custom_attributes_models.UserStoryCustomAttribute
+ exclude = ('id', 'project')
+
+
+class TaskCustomAttributeExportValidator(validators.ModelValidator):
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = custom_attributes_models.TaskCustomAttribute
+ exclude = ('id', 'project')
+
+
+class IssueCustomAttributeExportValidator(validators.ModelValidator):
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = custom_attributes_models.IssueCustomAttribute
+ exclude = ('id', 'project')
+
+
+class BaseCustomAttributesValuesExportValidator(validators.ModelValidator):
+ attributes_values = JsonField(source="attributes_values", required=True)
+ _custom_attribute_model = None
+ _container_field = None
+
+ class Meta:
+ exclude = ("id",)
+
+ def validate_attributes_values(self, attrs, source):
+ # values must be a dict
+ data_values = attrs.get("attributes_values", None)
+ if self.object:
+ data_values = (data_values or self.object.attributes_values)
+
+ if type(data_values) is not dict:
+ raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
+
+ # Values keys must be in the container object project
+ data_container = attrs.get(self._container_field, None)
+ if data_container:
+ project_id = data_container.project_id
+ elif self.object:
+ project_id = getattr(self.object, self._container_field).project_id
+ else:
+ project_id = None
+
+ values_ids = list(data_values.keys())
+ qs = self._custom_attribute_model.objects.filter(project=project_id,
+ id__in=values_ids)
+ if qs.count() != len(values_ids):
+ raise ValidationError(_("It contain invalid custom fields."))
+
+ return attrs
+
+
+class EpicCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator):
+ _custom_attribute_model = custom_attributes_models.EpicCustomAttribute
+ _container_model = "epics.Epic"
+ _container_field = "epic"
+
+ class Meta(BaseCustomAttributesValuesExportValidator.Meta):
+ model = custom_attributes_models.EpicCustomAttributesValues
+
+
+class UserStoryCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator):
+ _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
+ _container_model = "userstories.UserStory"
+ _container_field = "user_story"
+
+ class Meta(BaseCustomAttributesValuesExportValidator.Meta):
+ model = custom_attributes_models.UserStoryCustomAttributesValues
+
+
+class TaskCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator):
+ _custom_attribute_model = custom_attributes_models.TaskCustomAttribute
+ _container_field = "task"
+
+ class Meta(BaseCustomAttributesValuesExportValidator.Meta):
+ model = custom_attributes_models.TaskCustomAttributesValues
+
+
+class IssueCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator):
+ _custom_attribute_model = custom_attributes_models.IssueCustomAttribute
+ _container_field = "issue"
+
+ class Meta(BaseCustomAttributesValuesExportValidator.Meta):
+ model = custom_attributes_models.IssueCustomAttributesValues
+
+
+class MembershipExportValidator(validators.ModelValidator):
+ user = UserRelatedField(required=False)
+ role = ProjectRelatedField(slug_field="name")
+ invited_by = UserRelatedField(required=False)
+
+ class Meta:
+ model = projects_models.Membership
+ exclude = ('id', 'project', 'token')
+
+ def full_clean(self, instance):
+ return instance
+
+
+class RolePointsExportValidator(validators.ModelValidator):
+ role = ProjectRelatedField(slug_field="name")
+ points = ProjectRelatedField(slug_field="name")
+
+ class Meta:
+ model = userstories_models.RolePoints
+ exclude = ('id', 'user_story')
+
+
+class MilestoneExportValidator(WatcheableObjectModelValidatorMixin):
+ owner = UserRelatedField(required=False)
+ modified_date = serializers.DateTimeField(required=False)
+ estimated_start = serializers.DateField(required=False)
+ estimated_finish = serializers.DateField(required=False)
+
+ def __init__(self, *args, **kwargs):
+ project = kwargs.pop('project', None)
+ super(MilestoneExportValidator, self).__init__(*args, **kwargs)
+ if project:
+ self.project = project
+
+ def validate_name(self, attrs, source):
+ """
+ Check the milestone name is not duplicated in the project
+ """
+ name = attrs[source]
+ qs = self.project.milestones.filter(name=name)
+ if qs.exists():
+ raise ValidationError(_("Name duplicated for the project"))
+
+ return attrs
+
+ class Meta:
+ model = milestones_models.Milestone
+ exclude = ('id', 'project')
+
+
+class TaskExportValidator(WatcheableObjectModelValidatorMixin):
+ owner = UserRelatedField(required=False)
+ status = ProjectRelatedField(slug_field="name")
+ user_story = ProjectRelatedField(slug_field="ref", required=False)
+ milestone = ProjectRelatedField(slug_field="name", required=False)
+ assigned_to = UserRelatedField(required=False)
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = tasks_models.Task
+ exclude = ('id', 'project')
+
+ def custom_attributes_queryset(self, project):
+ if project.id not in _custom_tasks_attributes_cache:
+ _custom_tasks_attributes_cache[project.id] = list(project.taskcustomattributes.all().values('id', 'name'))
+ return _custom_tasks_attributes_cache[project.id]
+
+
+class EpicRelatedUserStoryExportValidator(validators.ModelValidator):
+ user_story = ProjectRelatedField(slug_field="ref")
+ order = serializers.IntegerField()
+
+ class Meta:
+ model = epics_models.RelatedUserStory
+ exclude = ('id', 'epic')
+
+
+class EpicExportValidator(WatcheableObjectModelValidatorMixin):
+ owner = UserRelatedField(required=False)
+ assigned_to = UserRelatedField(required=False)
+ status = ProjectRelatedField(slug_field="name")
+ modified_date = serializers.DateTimeField(required=False)
+ user_stories = EpicRelatedUserStoryExportValidator(many=True, required=False)
+
+ class Meta:
+ model = epics_models.Epic
+ exclude = ('id', 'project')
+
+ def custom_attributes_queryset(self, project):
+ if project.id not in _custom_epics_attributes_cache:
+ _custom_epics_attributes_cache[project.id] = list(
+ project.epiccustomattributes.all().values('id', 'name')
+ )
+ return _custom_epics_attributes_cache[project.id]
+
+
+class UserStoryExportValidator(WatcheableObjectModelValidatorMixin):
+ role_points = RolePointsExportValidator(many=True, required=False)
+ owner = UserRelatedField(required=False)
+ assigned_to = UserRelatedField(required=False)
+ status = ProjectRelatedField(slug_field="name")
+ milestone = ProjectRelatedField(slug_field="name", required=False)
+ modified_date = serializers.DateTimeField(required=False)
+ generated_from_issue = ProjectRelatedField(slug_field="ref", required=False)
+
+ class Meta:
+ model = userstories_models.UserStory
+ exclude = ('id', 'project', 'points', 'tasks')
+
+ def custom_attributes_queryset(self, project):
+ if project.id not in _custom_userstories_attributes_cache:
+ _custom_userstories_attributes_cache[project.id] = list(
+ project.userstorycustomattributes.all().values('id', 'name')
+ )
+ return _custom_userstories_attributes_cache[project.id]
+
+
+class IssueExportValidator(WatcheableObjectModelValidatorMixin):
+ owner = UserRelatedField(required=False)
+ status = ProjectRelatedField(slug_field="name")
+ assigned_to = UserRelatedField(required=False)
+ priority = ProjectRelatedField(slug_field="name")
+ severity = ProjectRelatedField(slug_field="name")
+ type = ProjectRelatedField(slug_field="name")
+ milestone = ProjectRelatedField(slug_field="name", required=False)
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = issues_models.Issue
+ exclude = ('id', 'project')
+
+ def custom_attributes_queryset(self, project):
+ if project.id not in _custom_issues_attributes_cache:
+ _custom_issues_attributes_cache[project.id] = list(project.issuecustomattributes.all().values('id', 'name'))
+ return _custom_issues_attributes_cache[project.id]
+
+
+class WikiPageExportValidator(WatcheableObjectModelValidatorMixin):
+ owner = UserRelatedField(required=False)
+ last_modifier = UserRelatedField(required=False)
+ modified_date = serializers.DateTimeField(required=False)
+
+ class Meta:
+ model = wiki_models.WikiPage
+ exclude = ('id', 'project')
+
+
+class WikiLinkExportValidator(validators.ModelValidator):
+ class Meta:
+ model = wiki_models.WikiLink
+ exclude = ('id', 'project')
+
+
+class TimelineExportValidator(validators.ModelValidator):
+ data = TimelineDataField()
+ data_content_type = ContentTypeField()
+
+ class Meta:
+ model = timeline_models.Timeline
+ exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')
+
+
+class ProjectExportValidator(WatcheableObjectModelValidatorMixin):
+ logo = FileField(required=False)
+ anon_permissions = PgArrayField(required=False)
+ public_permissions = PgArrayField(required=False)
+ modified_date = serializers.DateTimeField(required=False)
+ roles = RoleExportValidator(many=True, required=False)
+ owner = UserRelatedField(required=False)
+ memberships = MembershipExportValidator(many=True, required=False)
+ points = PointsExportValidator(many=True, required=False)
+ us_statuses = UserStoryStatusExportValidator(many=True, required=False)
+ task_statuses = TaskStatusExportValidator(many=True, required=False)
+ issue_types = IssueTypeExportValidator(many=True, required=False)
+ issue_statuses = IssueStatusExportValidator(many=True, required=False)
+ priorities = PriorityExportValidator(many=True, required=False)
+ severities = SeverityExportValidator(many=True, required=False)
+ tags_colors = JsonField(required=False)
+ creation_template = serializers.SlugRelatedField(slug_field="slug", required=False)
+ default_points = serializers.SlugRelatedField(slug_field="name", required=False)
+ default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
+ default_task_status = serializers.SlugRelatedField(slug_field="name", required=False)
+ default_priority = serializers.SlugRelatedField(slug_field="name", required=False)
+ default_severity = serializers.SlugRelatedField(slug_field="name", required=False)
+ default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False)
+ default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False)
+ userstorycustomattributes = UserStoryCustomAttributeExportValidator(many=True, required=False)
+ taskcustomattributes = TaskCustomAttributeExportValidator(many=True, required=False)
+ issuecustomattributes = IssueCustomAttributeExportValidator(many=True, required=False)
+ user_stories = UserStoryExportValidator(many=True, required=False)
+ tasks = TaskExportValidator(many=True, required=False)
+ milestones = MilestoneExportValidator(many=True, required=False)
+ issues = IssueExportValidator(many=True, required=False)
+ wiki_links = WikiLinkExportValidator(many=True, required=False)
+ wiki_pages = WikiPageExportValidator(many=True, required=False)
+
+ class Meta:
+ model = projects_models.Project
+ exclude = ('id', 'members')
diff --git a/taiga/external_apps/api.py b/taiga/external_apps/api.py
index 931337a8..8ded55d5 100644
--- a/taiga/external_apps/api.py
+++ b/taiga/external_apps/api.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from . import serializers
+from . import validators
from . import models
from . import permissions
from . import services
@@ -27,12 +28,12 @@ from taiga.base.api import ModelCrudViewSet, ModelRetrieveViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route, detail_route
-from django.db import transaction
from django.utils.translation import ugettext_lazy as _
class Application(ModelRetrieveViewSet):
serializer_class = serializers.ApplicationSerializer
+ validator_class = validators.ApplicationValidator
permission_classes = (permissions.ApplicationPermission,)
model = models.Application
@@ -61,6 +62,7 @@ class Application(ModelRetrieveViewSet):
class ApplicationToken(ModelCrudViewSet):
serializer_class = serializers.ApplicationTokenSerializer
+ validator_class = validators.ApplicationTokenValidator
permission_classes = (permissions.ApplicationTokenPermission,)
def get_queryset(self):
@@ -87,9 +89,9 @@ class ApplicationToken(ModelCrudViewSet):
auth_code = request.DATA.get("auth_code", None)
state = request.DATA.get("state", None)
application_token = get_object_or_404(models.ApplicationToken,
- application__id=application_id,
- auth_code=auth_code,
- state=state)
+ application__id=application_id,
+ auth_code=auth_code,
+ state=state)
application_token.generate_token()
application_token.save()
diff --git a/taiga/external_apps/serializers.py b/taiga/external_apps/serializers.py
index 095465fd..12ed3bab 100644
--- a/taiga/external_apps/serializers.py
+++ b/taiga/external_apps/serializers.py
@@ -16,9 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import json
-
from taiga.base.api import serializers
+from taiga.base.fields import Field
from . import models
from . import services
@@ -26,33 +25,27 @@ from . import services
from django.utils.translation import ugettext as _
-class ApplicationSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Application
- fields = ("id", "name", "web", "description", "icon_url")
+class ApplicationSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ web = Field()
+ description = Field()
+ icon_url = Field()
-class ApplicationTokenSerializer(serializers.ModelSerializer):
- cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
- next_url = serializers.CharField(source="next_url", read_only=True)
- application = ApplicationSerializer(read_only=True)
-
- class Meta:
- model = models.ApplicationToken
- fields = ("user", "id", "application", "auth_code", "next_url")
+class ApplicationTokenSerializer(serializers.LightSerializer):
+ id = Field()
+ user = Field(attr="user_id")
+ application = ApplicationSerializer()
+ auth_code = Field()
+ next_url = Field()
-class AuthorizationCodeSerializer(serializers.ModelSerializer):
- next_url = serializers.CharField(source="next_url", read_only=True)
- class Meta:
- model = models.ApplicationToken
- fields = ("auth_code", "state", "next_url")
+class AuthorizationCodeSerializer(serializers.LightSerializer):
+ state = Field()
+ auth_code = Field()
+ next_url = Field()
-class AccessTokenSerializer(serializers.ModelSerializer):
- cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
- next_url = serializers.CharField(source="next_url", read_only=True)
-
- class Meta:
- model = models.ApplicationToken
- fields = ("cyphered_token", )
+class AccessTokenSerializer(serializers.LightSerializer):
+ cyphered_token = Field()
diff --git a/taiga/external_apps/validators.py b/taiga/external_apps/validators.py
new file mode 100644
index 00000000..b2f2354d
--- /dev/null
+++ b/taiga/external_apps/validators.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import serializers
+
+from . import models
+from taiga.base.api import validators
+
+
+class ApplicationValidator(validators.ModelValidator):
+ class Meta:
+ model = models.Application
+ fields = ("id", "name", "web", "description", "icon_url")
+
+
+class ApplicationTokenValidator(validators.ModelValidator):
+ cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
+ next_url = serializers.CharField(source="next_url", read_only=True)
+ application = ApplicationValidator(read_only=True)
+
+ class Meta:
+ model = models.ApplicationToken
+ fields = ("user", "id", "application", "auth_code", "next_url")
+
+
+class AuthorizationCodeValidator(validators.ModelValidator):
+ next_url = serializers.CharField(source="next_url", read_only=True)
+ class Meta:
+ model = models.ApplicationToken
+ fields = ("auth_code", "state", "next_url")
+
+
+class AccessTokenValidator(validators.ModelValidator):
+ cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
+ next_url = serializers.CharField(source="next_url", read_only=True)
+
+ class Meta:
+ model = models.ApplicationToken
+ fields = ("cyphered_token", )
diff --git a/taiga/feedback/api.py b/taiga/feedback/api.py
index c477b5eb..0f573b87 100644
--- a/taiga/feedback/api.py
+++ b/taiga/feedback/api.py
@@ -20,7 +20,7 @@ from taiga.base import response
from taiga.base.api import viewsets
from . import permissions
-from . import serializers
+from . import validators
from . import services
import copy
@@ -28,7 +28,7 @@ import copy
class FeedbackViewSet(viewsets.ViewSet):
permission_classes = (permissions.FeedbackPermission,)
- serializer_class = serializers.FeedbackEntrySerializer
+ validator_class = validators.FeedbackEntryValidator
def create(self, request, **kwargs):
self.check_permissions(request, "create", None)
@@ -37,11 +37,11 @@ class FeedbackViewSet(viewsets.ViewSet):
data.update({"full_name": request.user.get_full_name(),
"email": request.user.email})
- serializer = self.serializer_class(data=data)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = self.validator_class(data=data)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- self.object = serializer.save(force_insert=True)
+ self.object = validator.save(force_insert=True)
extra = {
"HTTP_HOST": request.META.get("HTTP_HOST", None),
@@ -50,4 +50,4 @@ class FeedbackViewSet(viewsets.ViewSet):
}
services.send_feedback(self.object, extra, reply_to=[request.user.email])
- return response.Ok(serializer.data)
+ return response.Ok(validator.data)
diff --git a/taiga/feedback/serializers.py b/taiga/feedback/validators.py
similarity index 91%
rename from taiga/feedback/serializers.py
rename to taiga/feedback/validators.py
index 1b5f1a3e..7b31ec88 100644
--- a/taiga/feedback/serializers.py
+++ b/taiga/feedback/validators.py
@@ -16,11 +16,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api import serializers
+from taiga.base.api import validators
from . import models
-class FeedbackEntrySerializer(serializers.ModelSerializer):
+class FeedbackEntryValidator(validators.ModelValidator):
class Meta:
model = models.FeedbackEntry
diff --git a/taiga/front/sitemaps/__init__.py b/taiga/front/sitemaps/__init__.py
index abc78ffe..8c7adfa8 100644
--- a/taiga/front/sitemaps/__init__.py
+++ b/taiga/front/sitemaps/__init__.py
@@ -21,11 +21,14 @@ from collections import OrderedDict
from .generics import GenericSitemap
from .projects import ProjectsSitemap
+from .projects import ProjectEpicsSitemap
from .projects import ProjectBacklogsSitemap
from .projects import ProjectKanbansSitemap
from .projects import ProjectIssuesSitemap
from .projects import ProjectTeamsSitemap
+from .epics import EpicsSitemap
+
from .milestones import MilestonesSitemap
from .userstories import UserStoriesSitemap
@@ -43,11 +46,14 @@ sitemaps = OrderedDict([
("generics", GenericSitemap),
("projects", ProjectsSitemap),
+ ("project-epics-list", ProjectEpicsSitemap),
("project-backlogs", ProjectBacklogsSitemap),
("project-kanbans", ProjectKanbansSitemap),
("project-issues-list", ProjectIssuesSitemap),
("project-teams", ProjectTeamsSitemap),
+ ("epics", EpicsSitemap),
+
("milestones", MilestonesSitemap),
("userstories", UserStoriesSitemap),
diff --git a/taiga/front/sitemaps/epics.py b/taiga/front/sitemaps/epics.py
new file mode 100644
index 00000000..81f391f6
--- /dev/null
+++ b/taiga/front/sitemaps/epics.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.db.models import Q
+from django.apps import apps
+
+from taiga.front.templatetags.functions import resolve
+
+from .base import Sitemap
+
+
+class EpicsSitemap(Sitemap):
+ def items(self):
+ epic_model = apps.get_model("epics", "Epic")
+
+ # Get epics of public projects OR private projects if anon user can view them
+ queryset = epic_model.objects.filter(Q(project__is_private=False) |
+ Q(project__is_private=True,
+ project__anon_permissions__contains=["view_epics"]))
+
+ # Exclude blocked projects
+ queryset = queryset.filter(project__blocked_code__isnull=True)
+
+ # Project data is needed
+ queryset = queryset.select_related("project")
+
+ return queryset
+
+ def location(self, obj):
+ return resolve("epic", obj.project.slug, obj.ref)
+
+ def lastmod(self, obj):
+ return obj.modified_date
+
+ def changefreq(self, obj):
+ return "daily"
+
+ def priority(self, obj):
+ return 0.4
diff --git a/taiga/front/sitemaps/projects.py b/taiga/front/sitemaps/projects.py
index a7e45d50..77785928 100644
--- a/taiga/front/sitemaps/projects.py
+++ b/taiga/front/sitemaps/projects.py
@@ -51,6 +51,34 @@ class ProjectsSitemap(Sitemap):
return 0.9
+class ProjectEpicsSitemap(Sitemap):
+ def items(self):
+ project_model = apps.get_model("projects", "Project")
+
+ # Get public projects OR private projects if anon user can view them and epics
+ queryset = project_model.objects.filter(Q(is_private=False) |
+ Q(is_private=True,
+ anon_permissions__contains=["view_project",
+ "view_epics"]))
+
+ # Exclude projects without epics enabled
+ queryset = queryset.exclude(is_epics_activated=False)
+
+ return queryset
+
+ def location(self, obj):
+ return resolve("epics", obj.slug)
+
+ def lastmod(self, obj):
+ return obj.modified_date
+
+ def changefreq(self, obj):
+ return "daily"
+
+ def priority(self, obj):
+ return 0.6
+
+
class ProjectBacklogsSitemap(Sitemap):
def items(self):
project_model = apps.get_model("projects", "Project")
diff --git a/taiga/front/urls.py b/taiga/front/urls.py
index ab1cec8c..77d53dab 100644
--- a/taiga/front/urls.py
+++ b/taiga/front/urls.py
@@ -33,6 +33,9 @@ urls = {
"project": "/project/{0}", # project.slug
+ "epics": "/project/{0}/epics/", # project.slug
+ "epic": "/project/{0}/epic/{1}", # project.slug, epic.ref
+
"backlog": "/project/{0}/backlog/", # project.slug
"taskboard": "/project/{0}/taskboard/{1}", # project.slug, milestone.slug
"kanban": "/project/{0}/kanban/", # project.slug
diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py
index 24fc478c..07e2829b 100644
--- a/taiga/hooks/bitbucket/api.py
+++ b/taiga/hooks/bitbucket/api.py
@@ -72,13 +72,5 @@ class BitBucketViewSet(BaseWebhookApiViewSet):
return project_secret == secret_key
- def _get_project(self, request):
- project_id = request.GET.get("project", None)
- try:
- project = Project.objects.get(id=project_id)
- return project
- except Project.DoesNotExist:
- return None
-
def _get_event_name(self, request):
return request.META.get('HTTP_X_EVENT_KEY', None)
diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py
index 8737aaa7..67ffc3fd 100644
--- a/taiga/hooks/bitbucket/event_hooks.py
+++ b/taiga/hooks/bitbucket/event_hooks.py
@@ -18,181 +18,67 @@
import re
-from django.utils.translation import ugettext as _
-
-from taiga.base import exceptions as exc
-from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus
-from taiga.projects.issues.models import Issue
-from taiga.projects.tasks.models import Task
-from taiga.projects.userstories.models import UserStory
-from taiga.projects.history.services import take_snapshot
-from taiga.projects.notifications.services import send_notifications
-from taiga.hooks.event_hooks import BaseEventHook
-from taiga.hooks.exceptions import ActionSyntaxException
-from taiga.base.utils import json
-
-from .services import get_bitbucket_user
+from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook
-class PushEventHook(BaseEventHook):
- def process_event(self):
- if self.payload is None:
- return
+class BaseBitBucketEventHook():
+ platform = "BitBucket"
+ platform_slug = "bitbucket"
+ def replace_bitbucket_references(self, project_url, wiki_text):
+ if wiki_text is None:
+ wiki_text = ""
+
+ template = "\g<1>[BitBucket#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
+ return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
+
+
+class IssuesEventHook(BaseBitBucketEventHook, BaseNewIssueEventHook):
+ def get_data(self):
+ description = self.payload.get('issue', {}).get('content', {}).get('raw', '')
+ project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None)
+ return {
+ "number": self.payload.get('issue', {}).get('id', None),
+ "subject": self.payload.get('issue', {}).get('title', None),
+ "url": self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None),
+ "user_id": self.payload.get('actor', {}).get('uuid', None),
+ "user_name": self.payload.get('actor', {}).get('username', None),
+ "user_url": self.payload.get('actor', {}).get('links', {}).get('html', {}).get('href'),
+ "description": self.replace_bitbucket_references(project_url, description),
+ }
+
+
+class IssueCommentEventHook(BaseBitBucketEventHook, BaseIssueCommentEventHook):
+ def get_data(self):
+ comment_message = self.payload.get('comment', {}).get('content', {}).get('raw', '')
+ project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None)
+ issue_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None)
+ comment_id = self.payload.get('comment', {}).get('id', None)
+ comment_url = "{}#comment-{}".format(issue_url, comment_id)
+ return {
+ "number": self.payload.get('issue', {}).get('id', None),
+ 'url': issue_url,
+ 'user_id': self.payload.get('actor', {}).get('uuid', None),
+ 'user_name': self.payload.get('actor', {}).get('username', None),
+ 'user_url': self.payload.get('actor', {}).get('links', {}).get('html', {}).get('href'),
+ 'comment_url': comment_url,
+ 'comment_message': self.replace_bitbucket_references(project_url, comment_message)
+ }
+
+
+class PushEventHook(BaseBitBucketEventHook, BasePushEventHook):
+ def get_data(self):
+ result = []
changes = self.payload.get("push", {}).get('changes', [])
for change in filter(None, changes):
- commits = change.get("commits", [])
- if not commits:
- continue
-
- for commit in commits:
- message = commit.get("message", None)
- if not message:
- continue
-
- self._process_message(message, None)
-
- def _process_message(self, message, bitbucket_user):
- """
- The message we will be looking for seems like
- TG-XX #yyyyyy
- Where:
- XX: is the ref for us, issue or task
- yyyyyy: is the status slug we are setting
- """
- if message is None:
- return
-
- p = re.compile("tg-(\d+) +#([-\w]+)")
- for m in p.finditer(message.lower()):
- ref = m.group(1)
- status_slug = m.group(2)
- self._change_status(ref, status_slug, bitbucket_user)
-
- def _change_status(self, ref, status_slug, bitbucket_user):
- if Issue.objects.filter(project=self.project, ref=ref).exists():
- modelClass = Issue
- statusClass = IssueStatus
- elif Task.objects.filter(project=self.project, ref=ref).exists():
- modelClass = Task
- statusClass = TaskStatus
- elif UserStory.objects.filter(project=self.project, ref=ref).exists():
- modelClass = UserStory
- statusClass = UserStoryStatus
- else:
- raise ActionSyntaxException(_("The referenced element doesn't exist"))
-
- element = modelClass.objects.get(project=self.project, ref=ref)
-
- try:
- status = statusClass.objects.get(project=self.project, slug=status_slug)
- except statusClass.DoesNotExist:
- raise ActionSyntaxException(_("The status doesn't exist"))
-
- element.status = status
- element.save()
-
- snapshot = take_snapshot(element,
- comment=_("Status changed from BitBucket commit"),
- user=get_bitbucket_user(bitbucket_user))
- send_notifications(element, history=snapshot)
-
-
-def replace_bitbucket_references(project_url, wiki_text):
- template = "\g<1>[BitBucket#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
- return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
-
-
-class IssuesEventHook(BaseEventHook):
- def process_event(self):
- number = self.payload.get('issue', {}).get('id', None)
- subject = self.payload.get('issue', {}).get('title', None)
-
- bitbucket_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None)
-
- bitbucket_user_id = self.payload.get('actor', {}).get('user', {}).get('uuid', None)
- bitbucket_user_name = self.payload.get('actor', {}).get('user', {}).get('username', None)
- bitbucket_user_url = self.payload.get('actor', {}).get('user', {}).get('links', {}).get('html', {}).get('href')
-
- project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None)
-
- description = self.payload.get('issue', {}).get('content', {}).get('raw', '')
- description = replace_bitbucket_references(project_url, description)
-
- user = get_bitbucket_user(bitbucket_user_id)
-
- if not all([subject, bitbucket_url, project_url]):
- raise ActionSyntaxException(_("Invalid issue information"))
-
- issue = Issue.objects.create(
- project=self.project,
- subject=subject,
- description=description,
- status=self.project.default_issue_status,
- type=self.project.default_issue_type,
- severity=self.project.default_severity,
- priority=self.project.default_priority,
- external_reference=['bitbucket', bitbucket_url],
- owner=user
- )
- take_snapshot(issue, user=user)
-
- if number and subject and bitbucket_user_name and bitbucket_user_url:
- comment = _("Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} "
- "\"See @{bitbucket_user_name}'s BitBucket profile\") "
- "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} "
- "\"Go to 'bb#{number} - {subject}'\"):\n\n"
- "{description}").format(bitbucket_user_name=bitbucket_user_name,
- bitbucket_user_url=bitbucket_user_url,
- number=number,
- subject=subject,
- bitbucket_url=bitbucket_url,
- description=description)
- else:
- comment = _("Issue created from BitBucket.")
-
- snapshot = take_snapshot(issue, comment=comment, user=user)
- send_notifications(issue, history=snapshot)
-
-
-class IssueCommentEventHook(BaseEventHook):
- def process_event(self):
- number = self.payload.get('issue', {}).get('id', None)
- subject = self.payload.get('issue', {}).get('title', None)
-
- bitbucket_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None)
- bitbucket_user_id = self.payload.get('actor', {}).get('user', {}).get('uuid', None)
- bitbucket_user_name = self.payload.get('actor', {}).get('user', {}).get('username', None)
- bitbucket_user_url = self.payload.get('actor', {}).get('user', {}).get('links', {}).get('html', {}).get('href')
-
- project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None)
-
- comment_message = self.payload.get('comment', {}).get('content', {}).get('raw', '')
- comment_message = replace_bitbucket_references(project_url, comment_message)
-
- user = get_bitbucket_user(bitbucket_user_id)
-
- if not all([comment_message, bitbucket_url, project_url]):
- raise ActionSyntaxException(_("Invalid issue comment information"))
-
- issues = Issue.objects.filter(external_reference=["bitbucket", bitbucket_url])
- tasks = Task.objects.filter(external_reference=["bitbucket", bitbucket_url])
- uss = UserStory.objects.filter(external_reference=["bitbucket", bitbucket_url])
-
- for item in list(issues) + list(tasks) + list(uss):
- if number and subject and bitbucket_user_name and bitbucket_user_url:
- comment = _("Comment by [@{bitbucket_user_name}]({bitbucket_user_url} "
- "\"See @{bitbucket_user_name}'s BitBucket profile\") "
- "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} "
- "\"Go to 'bb#{number} - {subject}'\")\n\n"
- "{message}").format(bitbucket_user_name=bitbucket_user_name,
- bitbucket_user_url=bitbucket_user_url,
- number=number,
- subject=subject,
- bitbucket_url=bitbucket_url,
- message=comment_message)
- else:
- comment = _("Comment From BitBucket:\n\n{message}").format(message=comment_message)
-
- snapshot = take_snapshot(item, comment=comment, user=user)
- send_notifications(item, history=snapshot)
+ for commit in change.get("commits", []):
+ message = commit.get("message")
+ result.append({
+ 'user_id': commit.get('author', {}).get('user', {}).get('uuid', None),
+ "user_name": commit.get('author', {}).get('user', {}).get('username', None),
+ "user_url": commit.get('author', {}).get('user', {}).get('links', {}).get('html', {}).get('href'),
+ "commit_id": commit.get("hash", None),
+ "commit_url": commit.get("links", {}).get('html', {}).get('href'),
+ "commit_message": message.strip(),
+ })
+ return result
diff --git a/taiga/hooks/event_hooks.py b/taiga/hooks/event_hooks.py
index f4f6d2e8..93deb518 100644
--- a/taiga/hooks/event_hooks.py
+++ b/taiga/hooks/event_hooks.py
@@ -16,11 +16,251 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import re
+
+from django.utils.translation import ugettext as _
+from django.contrib.auth import get_user_model
+from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus, EpicStatus
+from taiga.projects.epics.models import Epic
+from taiga.projects.issues.models import Issue
+from taiga.projects.tasks.models import Task
+from taiga.projects.userstories.models import UserStory
+from taiga.projects.history.services import take_snapshot
+from taiga.projects.notifications.services import send_notifications
+from taiga.hooks.exceptions import ActionSyntaxException
+from taiga.users.models import AuthData
+
class BaseEventHook:
+ platform = "Unknown"
+ platform_slug = "unknown"
+
def __init__(self, project, payload):
self.project = project
self.payload = payload
+ def ignore(self):
+ return False
+
+ def get_user(self, user_id, platform):
+ user = None
+
+ if user_id:
+ try:
+ user = AuthData.objects.get(key=platform, value=user_id).user
+ except AuthData.DoesNotExist:
+ pass
+
+ if user is None:
+ user = get_user_model().objects.get(is_system=True, username__startswith=platform)
+
+ return user
+
+
+class BaseIssueCommentEventHook(BaseEventHook):
+ def get_data(self):
+ raise NotImplementedError
+
+ def generate_issue_comment_message(self, **kwargs):
+ _issue_comment_message = _(
+ "[@{user_name}]({user_url} "
+ "\"See @{user_name}'s {platform} profile\") "
+ "says in [{platform}#{number}]({comment_url} \"Go to comment\"):\n\n"
+ "\"{comment_message}\""
+ )
+ _simple_issue_comment_message = _("Comment From {platform}:\n\n> {comment_message}")
+ try:
+ return _issue_comment_message.format(platform=self.platform, **kwargs)
+ except Exception:
+ return _simple_issue_comment_message.format(platform=self.platform, message=kwargs.get('comment_message'))
+
def process_event(self):
- raise NotImplementedError("process_event must be overwritten")
+ if self.ignore():
+ return
+
+ data = self.get_data()
+
+ if not all([data['comment_message'], data['url']]):
+ raise ActionSyntaxException(_("Invalid issue comment information"))
+
+ comment = self.generate_issue_comment_message(**data)
+
+ issues = Issue.objects.filter(external_reference=[self.platform_slug, data['url']])
+ tasks = Task.objects.filter(external_reference=[self.platform_slug, data['url']])
+ uss = UserStory.objects.filter(external_reference=[self.platform_slug, data['url']])
+
+ for item in list(issues) + list(tasks) + list(uss):
+ snapshot = take_snapshot(item, comment=comment, user=self.get_user(data['user_id'], self.platform_slug))
+ send_notifications(item, history=snapshot)
+
+
+class BaseNewIssueEventHook(BaseEventHook):
+ def get_data(self):
+ raise NotImplementedError
+
+ def generate_new_issue_comment(self, **kwargs):
+ _new_issue_message = _(
+ "Issue created by [@{user_name}]({user_url} "
+ "\"See @{user_name}'s {platform} profile\") "
+ "from [{platform}#{number}]({url} \"Go to issue\")."
+ )
+ _simple_new_issue_message = _("Issue created from {platform}.")
+ try:
+ return _new_issue_message.format(platform=self.platform, **kwargs)
+ except Exception:
+ return _simple_new_issue_message.format(platform=self.platform)
+
+ def process_event(self):
+ if self.ignore():
+ return
+
+ data = self.get_data()
+
+ if not all([data['subject'], data['url']]):
+ raise ActionSyntaxException(_("Invalid issue information"))
+
+ user = self.get_user(data['user_id'], self.platform_slug)
+
+ issue = Issue.objects.create(
+ project=self.project,
+ subject=data['subject'],
+ description=data['description'],
+ status=self.project.default_issue_status,
+ type=self.project.default_issue_type,
+ severity=self.project.default_severity,
+ priority=self.project.default_priority,
+ external_reference=[self.platform_slug, data['url']],
+ owner=user
+ )
+ take_snapshot(issue, user=user)
+
+ comment = self.generate_new_issue_comment(**data)
+
+ snapshot = take_snapshot(issue, comment=comment, user=user)
+ send_notifications(issue, history=snapshot)
+
+
+class BasePushEventHook(BaseEventHook):
+ def get_data(self):
+ raise NotImplementedError
+
+ def generate_status_change_comment(self, **kwargs):
+ if kwargs.get('user_url', None) is None:
+ user_text = kwargs.get('user_name', _('unknown user'))
+ else:
+ user_text = "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\")".format(
+ platform=self.platform,
+ **kwargs
+ )
+ _status_change_message = _(
+ "{user_text} changed the status from "
+ "[{platform} commit]({commit_url} \"See commit '{commit_id} - {commit_message}'\")\n\n"
+ " - Status: **{src_status}** → **{dst_status}**"
+ )
+ _simple_status_change_message = _(
+ "Changed status from {platform} commit.\n\n"
+ " - Status: **{src_status}** → **{dst_status}**"
+ )
+ try:
+ return _status_change_message.format(platform=self.platform, user_text=user_text, **kwargs)
+ except Exception:
+ return _simple_status_change_message.format(platform=self.platform)
+
+ def generate_commit_reference_comment(self, **kwargs):
+ if kwargs.get('user_url', None) is None:
+ user_text = kwargs.get('user_name', _('unknown user'))
+ else:
+ user_text = "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\")".format(
+ platform=self.platform,
+ **kwargs
+ )
+
+ _status_change_message = _(
+ "This {type_name} has been mentioned by {user_text} "
+ "in the [{platform} commit]({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+ "\"{commit_message}\""
+ )
+ _simple_status_change_message = _(
+ "This issue has been mentioned in the {platform} commit "
+ "\"{commit_message}\""
+ )
+ try:
+ return _status_change_message.format(platform=self.platform, user_text=user_text, **kwargs)
+ except Exception:
+ return _simple_status_change_message.format(platform=self.platform)
+
+ def get_item_classes(self, ref):
+ if Epic.objects.filter(project=self.project, ref=ref).exists():
+ modelClass = Epic
+ statusClass = EpicStatus
+ elif Issue.objects.filter(project=self.project, ref=ref).exists():
+ modelClass = Issue
+ statusClass = IssueStatus
+ elif Task.objects.filter(project=self.project, ref=ref).exists():
+ modelClass = Task
+ statusClass = TaskStatus
+ elif UserStory.objects.filter(project=self.project, ref=ref).exists():
+ modelClass = UserStory
+ statusClass = UserStoryStatus
+ else:
+ raise ActionSyntaxException(_("The referenced element doesn't exist"))
+
+ return (modelClass, statusClass)
+
+ def get_item_by_ref(self, ref):
+ (modelClass, statusClass) = self.get_item_classes(ref)
+
+ return modelClass.objects.get(project=self.project, ref=ref)
+
+ def set_item_status(self, ref, status_slug):
+ (modelClass, statusClass) = self.get_item_classes(ref)
+ element = modelClass.objects.get(project=self.project, ref=ref)
+
+ try:
+ status = statusClass.objects.get(project=self.project, slug=status_slug)
+ except statusClass.DoesNotExist:
+ raise ActionSyntaxException(_("The status doesn't exist"))
+
+ src_status = element.status.name
+ dst_status = status.name
+
+ element.status = status
+ element.save()
+ return (element, src_status, dst_status)
+
+ def process_event(self):
+ if self.ignore():
+ return
+ data = self.get_data()
+
+ for commit in data:
+ consumed_refs = []
+
+ # Status changes
+ p = re.compile("tg-(\d+) +#([-\w]+)")
+ for m in p.finditer(commit['commit_message'].lower()):
+ ref = m.group(1)
+ status_slug = m.group(2)
+ (element, src_status, dst_status) = self.set_item_status(ref, status_slug)
+
+ comment = self.generate_status_change_comment(src_status=src_status, dst_status=dst_status, **commit)
+ snapshot = take_snapshot(element,
+ comment=comment,
+ user=self.get_user(commit['user_id'], self.platform_slug))
+ send_notifications(element, history=snapshot)
+ consumed_refs.append(ref)
+
+ # Reference on commit
+ p = re.compile("tg-(\d+)")
+ for m in p.finditer(commit['commit_message'].lower()):
+ ref = m.group(1)
+ if ref in consumed_refs:
+ continue
+ element = self.get_item_by_ref(ref)
+ type_name = element.__class__._meta.verbose_name
+ comment = self.generate_commit_reference_comment(type_name=type_name, **commit)
+ snapshot = take_snapshot(element,
+ comment=comment,
+ user=self.get_user(commit['user_id'], self.platform_slug))
+ send_notifications(element, history=snapshot)
+ consumed_refs.append(ref)
diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py
index 68e57993..c4ecc300 100644
--- a/taiga/hooks/github/event_hooks.py
+++ b/taiga/hooks/github/event_hooks.py
@@ -16,201 +16,72 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.utils.translation import ugettext as _
-
-from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus
-
-from taiga.projects.issues.models import Issue
-from taiga.projects.tasks.models import Task
-from taiga.projects.userstories.models import UserStory
-from taiga.projects.history.services import take_snapshot
-from taiga.projects.notifications.services import send_notifications
-from taiga.hooks.event_hooks import BaseEventHook
-from taiga.hooks.exceptions import ActionSyntaxException
-
-from .services import get_github_user
-
import re
-
-class PushEventHook(BaseEventHook):
- def process_event(self):
- if self.payload is None:
- return
-
- github_user = self.payload.get('sender', {})
-
- commits = self.payload.get("commits", [])
- for commit in commits:
- self._process_commit(commit, github_user)
-
- def _process_commit(self, commit, github_user):
- """
- The message we will be looking for seems like
- TG-XX #yyyyyy
- Where:
- XX: is the ref for us, issue or task
- yyyyyy: is the status slug we are setting
- """
- message = commit.get("message", None)
-
- if message is None:
- return
-
- p = re.compile("tg-(\d+) +#([-\w]+)")
- for m in p.finditer(message.lower()):
- ref = m.group(1)
- status_slug = m.group(2)
- self._change_status(ref, status_slug, github_user, commit)
-
- def _change_status(self, ref, status_slug, github_user, commit):
- if Issue.objects.filter(project=self.project, ref=ref).exists():
- modelClass = Issue
- statusClass = IssueStatus
- elif Task.objects.filter(project=self.project, ref=ref).exists():
- modelClass = Task
- statusClass = TaskStatus
- elif UserStory.objects.filter(project=self.project, ref=ref).exists():
- modelClass = UserStory
- statusClass = UserStoryStatus
- else:
- raise ActionSyntaxException(_("The referenced element doesn't exist"))
-
- element = modelClass.objects.get(project=self.project, ref=ref)
-
- try:
- status = statusClass.objects.get(project=self.project, slug=status_slug)
- except statusClass.DoesNotExist:
- raise ActionSyntaxException(_("The status doesn't exist"))
-
- element.status = status
- element.save()
-
- github_user_id = github_user.get('id', None)
- github_user_name = github_user.get('login', None)
- github_user_url = github_user.get('html_url', None)
- commit_id = commit.get("id", None)
- commit_url = commit.get("url", None)
- commit_message = commit.get("message", None)
-
- if (github_user_id and github_user_name and github_user_url and
- commit_id and commit_url and commit_message):
- comment = _("Status changed by [@{github_user_name}]({github_user_url} "
- "\"See @{github_user_name}'s GitHub profile\") "
- "from GitHub commit [{commit_id}]({commit_url} "
- "\"See commit '{commit_id} - {commit_message}'\").").format(
- github_user_name=github_user_name,
- github_user_url=github_user_url,
- commit_id=commit_id[:7],
- commit_url=commit_url,
- commit_message=commit_message)
-
- else:
- comment = _("Status changed from GitHub commit.")
-
- snapshot = take_snapshot(element,
- comment=comment,
- user=get_github_user(github_user_id))
- send_notifications(element, history=snapshot)
+from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook
-def replace_github_references(project_url, wiki_text):
- if wiki_text == None:
- wiki_text = ""
+class BaseGitHubEventHook():
+ platform = "GitHub"
+ platform_slug = "github"
- template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
- return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
+ def replace_github_references(self, project_url, wiki_text):
+ if wiki_text is None:
+ wiki_text = ""
+
+ template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
+ return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
-class IssuesEventHook(BaseEventHook):
- def process_event(self):
- if self.payload.get('action', None) != "opened":
- return
+class IssuesEventHook(BaseGitHubEventHook, BaseNewIssueEventHook):
+ def ignore(self):
+ return self.payload.get('action', None) != "opened"
- number = self.payload.get('issue', {}).get('number', None)
- subject = self.payload.get('issue', {}).get('title', None)
- github_url = self.payload.get('issue', {}).get('html_url', None)
- github_user_id = self.payload.get('issue', {}).get('user', {}).get('id', None)
- github_user_name = self.payload.get('issue', {}).get('user', {}).get('login', None)
- github_user_url = self.payload.get('issue', {}).get('user', {}).get('html_url', None)
- project_url = self.payload.get('repository', {}).get('html_url', None)
+ def get_data(self):
description = self.payload.get('issue', {}).get('body', None)
- description = replace_github_references(project_url, description)
-
- user = get_github_user(github_user_id)
-
- if not all([subject, github_url, project_url]):
- raise ActionSyntaxException(_("Invalid issue information"))
-
- issue = Issue.objects.create(
- project=self.project,
- subject=subject,
- description=description,
- status=self.project.default_issue_status,
- type=self.project.default_issue_type,
- severity=self.project.default_severity,
- priority=self.project.default_priority,
- external_reference=['github', github_url],
- owner=user
- )
- take_snapshot(issue, user=user)
-
- if number and subject and github_user_name and github_user_url:
- comment = _("Issue created by [@{github_user_name}]({github_user_url} "
- "\"See @{github_user_name}'s GitHub profile\") "
- "from GitHub.\nOrigin GitHub issue: [gh#{number} - {subject}]({github_url} "
- "\"Go to 'gh#{number} - {subject}'\"):\n\n"
- "{description}").format(github_user_name=github_user_name,
- github_user_url=github_user_url,
- number=number,
- subject=subject,
- github_url=github_url,
- description=description)
- else:
- comment = _("Issue created from GitHub.")
-
- snapshot = take_snapshot(issue, comment=comment, user=user)
- send_notifications(issue, history=snapshot)
-
-
-class IssueCommentEventHook(BaseEventHook):
- def process_event(self):
- if self.payload.get('action', None) != "created":
- raise ActionSyntaxException(_("Invalid issue comment information"))
-
- number = self.payload.get('issue', {}).get('number', None)
- subject = self.payload.get('issue', {}).get('title', None)
- github_url = self.payload.get('issue', {}).get('html_url', None)
- github_user_id = self.payload.get('sender', {}).get('id', None)
- github_user_name = self.payload.get('sender', {}).get('login', None)
- github_user_url = self.payload.get('sender', {}).get('html_url', None)
project_url = self.payload.get('repository', {}).get('html_url', None)
+ return {
+ "number": self.payload.get('issue', {}).get('number', None),
+ "subject": self.payload.get('issue', {}).get('title', None),
+ "url": self.payload.get('issue', {}).get('html_url', None),
+ "user_id": self.payload.get('issue', {}).get('user', {}).get('id', None),
+ "user_name": self.payload.get('issue', {}).get('user', {}).get('login', None),
+ "user_url": self.payload.get('issue', {}).get('user', {}).get('html_url', None),
+ "description": self.replace_github_references(project_url, description),
+ }
+
+
+class IssueCommentEventHook(BaseGitHubEventHook, BaseIssueCommentEventHook):
+ def ignore(self):
+ return self.payload.get('action', None) != "created"
+
+ def get_data(self):
comment_message = self.payload.get('comment', {}).get('body', None)
- comment_message = replace_github_references(project_url, comment_message)
+ project_url = self.payload.get('repository', {}).get('html_url', None)
+ return {
+ "number": self.payload.get('issue', {}).get('number', None),
+ "url": self.payload.get('issue', {}).get('html_url', None),
+ "user_id": self.payload.get('sender', {}).get('id', None),
+ "user_name": self.payload.get('sender', {}).get('login', None),
+ "user_url": self.payload.get('sender', {}).get('html_url', None),
+ "comment_url": self.payload.get('comment', {}).get('html_url', None),
+ "comment_message": self.replace_github_references(project_url, comment_message),
+ }
- user = get_github_user(github_user_id)
- if not all([comment_message, github_url, project_url]):
- raise ActionSyntaxException(_("Invalid issue comment information"))
+class PushEventHook(BaseGitHubEventHook, BasePushEventHook):
+ def get_data(self):
+ result = []
+ github_user = self.payload.get('sender', {})
+ commits = self.payload.get("commits", [])
+ for commit in filter(None, commits):
+ result.append({
+ "user_id": github_user.get('id', None),
+ "user_name": github_user.get('login', None),
+ "user_url": github_user.get('html_url', None),
+ "commit_id": commit.get("id", None),
+ "commit_url": commit.get("url", None),
+ "commit_message": commit.get("message", None),
+ })
- issues = Issue.objects.filter(external_reference=["github", github_url])
- tasks = Task.objects.filter(external_reference=["github", github_url])
- uss = UserStory.objects.filter(external_reference=["github", github_url])
-
- for item in list(issues) + list(tasks) + list(uss):
- if number and subject and github_user_name and github_user_url:
- comment = _("Comment by [@{github_user_name}]({github_user_url} "
- "\"See @{github_user_name}'s GitHub profile\") "
- "from GitHub.\nOrigin GitHub issue: [gh#{number} - {subject}]({github_url} "
- "\"Go to 'gh#{number} - {subject}'\")\n\n"
- "{message}").format(github_user_name=github_user_name,
- github_user_url=github_user_url,
- number=number,
- subject=subject,
- github_url=github_url,
- message=comment_message)
- else:
- comment = _("Comment From GitHub:\n\n{message}").format(message=comment_message)
-
- snapshot = take_snapshot(item, comment=comment, user=user)
- send_notifications(item, history=snapshot)
+ return result
diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py
index cd244ae3..e7286d86 100644
--- a/taiga/hooks/github/services.py
+++ b/taiga/hooks/github/services.py
@@ -18,10 +18,8 @@
import uuid
-from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
-from taiga.users.models import AuthData
from taiga.base.utils.urls import get_absolute_url
@@ -38,18 +36,3 @@ def get_or_generate_config(project):
url = "%s?project=%s" % (url, project.id)
g_config["webhooks_url"] = url
return g_config
-
-
-def get_github_user(github_id):
- user = None
-
- if github_id:
- try:
- user = AuthData.objects.get(key="github", value=github_id).user
- except AuthData.DoesNotExist:
- pass
-
- if user is None:
- user = get_user_model().objects.get(is_system=True, username__startswith="github")
-
- return user
diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py
index 910ee437..127d7536 100644
--- a/taiga/hooks/gitlab/api.py
+++ b/taiga/hooks/gitlab/api.py
@@ -70,14 +70,6 @@ class GitLabViewSet(BaseWebhookApiViewSet):
return project_secret == secret_key
- def _get_project(self, request):
- project_id = request.GET.get("project", None)
- try:
- project = Project.objects.get(id=project_id)
- return project
- except Project.DoesNotExist:
- return None
-
def _get_event_name(self, request):
payload = json.loads(request.body.decode("utf-8"))
return payload.get('object_kind', 'push') if payload is not None else 'empty'
diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py
index aff09e2f..5b4b4006 100644
--- a/taiga/hooks/gitlab/event_hooks.py
+++ b/taiga/hooks/gitlab/event_hooks.py
@@ -19,158 +19,71 @@
import re
import os
-from django.utils.translation import ugettext as _
-
-from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus
-
-from taiga.projects.issues.models import Issue
-from taiga.projects.tasks.models import Task
-from taiga.projects.userstories.models import UserStory
-from taiga.projects.history.services import take_snapshot
-from taiga.projects.notifications.services import send_notifications
-from taiga.hooks.event_hooks import BaseEventHook
-from taiga.hooks.exceptions import ActionSyntaxException
-
-from .services import get_gitlab_user
+from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook
-class PushEventHook(BaseEventHook):
- def process_event(self):
- if self.payload is None:
- return
+class BaseGitLabEventHook():
+ platform = "GitLab"
+ platform_slug = "gitlab"
- commits = self.payload.get("commits", [])
- for commit in commits:
- message = commit.get("message", None)
- self._process_message(message, None)
+ def replace_gitlab_references(self, project_url, wiki_text):
+ if wiki_text is None:
+ wiki_text = ""
- def _process_message(self, message, gitlab_user):
- """
- The message we will be looking for seems like
- TG-XX #yyyyyy
- Where:
- XX: is the ref for us, issue or task
- yyyyyy: is the status slug we are setting
- """
- if message is None:
- return
-
- p = re.compile("tg-(\d+) +#([-\w]+)")
- for m in p.finditer(message.lower()):
- ref = m.group(1)
- status_slug = m.group(2)
- self._change_status(ref, status_slug, gitlab_user)
-
- def _change_status(self, ref, status_slug, gitlab_user):
- if Issue.objects.filter(project=self.project, ref=ref).exists():
- modelClass = Issue
- statusClass = IssueStatus
- elif Task.objects.filter(project=self.project, ref=ref).exists():
- modelClass = Task
- statusClass = TaskStatus
- elif UserStory.objects.filter(project=self.project, ref=ref).exists():
- modelClass = UserStory
- statusClass = UserStoryStatus
- else:
- raise ActionSyntaxException(_("The referenced element doesn't exist"))
-
- element = modelClass.objects.get(project=self.project, ref=ref)
-
- try:
- status = statusClass.objects.get(project=self.project, slug=status_slug)
- except statusClass.DoesNotExist:
- raise ActionSyntaxException(_("The status doesn't exist"))
-
- element.status = status
- element.save()
-
- snapshot = take_snapshot(element,
- comment=_("Status changed from GitLab commit"),
- user=get_gitlab_user(gitlab_user))
- send_notifications(element, history=snapshot)
+ template = "\g<1>[GitLab#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
+ return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
-def replace_gitlab_references(project_url, wiki_text):
- if wiki_text is None:
- wiki_text = ""
+class IssuesEventHook(BaseGitLabEventHook, BaseNewIssueEventHook):
+ def ignore(self):
+ return self.payload.get('object_attributes', {}).get("action", "") != "open"
- template = "\g<1>[GitLab#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
- return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
-
-
-class IssuesEventHook(BaseEventHook):
- def process_event(self):
- if self.payload.get('object_attributes', {}).get("action", "") != "open":
- return
-
- subject = self.payload.get('object_attributes', {}).get('title', None)
+ def get_data(self):
description = self.payload.get('object_attributes', {}).get('description', None)
- gitlab_reference = self.payload.get('object_attributes', {}).get('url', None)
-
- project_url = None
- if gitlab_reference:
- project_url = os.path.basename(os.path.basename(gitlab_reference))
-
- if not all([subject, gitlab_reference, project_url]):
- raise ActionSyntaxException(_("Invalid issue information"))
-
- issue = Issue.objects.create(
- project=self.project,
- subject=subject,
- description=replace_gitlab_references(project_url, description),
- status=self.project.default_issue_status,
- type=self.project.default_issue_type,
- severity=self.project.default_severity,
- priority=self.project.default_priority,
- external_reference=['gitlab', gitlab_reference],
- owner=get_gitlab_user(None)
- )
- take_snapshot(issue, user=get_gitlab_user(None))
-
- snapshot = take_snapshot(issue, comment=_("Created from GitLab"), user=get_gitlab_user(None))
- send_notifications(issue, history=snapshot)
-
-
-class IssueCommentEventHook(BaseEventHook):
- def process_event(self):
- if self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue":
- return
-
- number = self.payload.get('issue', {}).get('iid', None)
- subject = self.payload.get('issue', {}).get('title', None)
-
project_url = self.payload.get('repository', {}).get('homepage', None)
+ user_name = self.payload.get('user', {}).get('username', None)
+ return {
+ "number": self.payload.get('object_attributes', {}).get('iid', None),
+ "subject": self.payload.get('object_attributes', {}).get('title', None),
+ "url": self.payload.get('object_attributes', {}).get('url', None),
+ "user_id": None,
+ "user_name": user_name,
+ "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", user_name),
+ "description": self.replace_gitlab_references(project_url, description),
+ }
- gitlab_url = os.path.join(project_url, "issues", str(number))
- gitlab_user_name = self.payload.get('user', {}).get('username', None)
- gitlab_user_url = os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", gitlab_user_name)
+class IssueCommentEventHook(BaseGitLabEventHook, BaseIssueCommentEventHook):
+ def ignore(self):
+ return self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue"
+
+ def get_data(self):
comment_message = self.payload.get('object_attributes', {}).get('note', None)
- comment_message = replace_gitlab_references(project_url, comment_message)
+ project_url = self.payload.get('repository', {}).get('homepage', None)
+ number = self.payload.get('issue', {}).get('iid', None)
+ user_name = self.payload.get('user', {}).get('username', None)
+ return {
+ "number": number,
+ "url": os.path.join(project_url, "issues", str(number)),
+ "user_id": None,
+ "user_name": user_name,
+ "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", user_name),
+ "comment_url": self.payload.get('object_attributes', {}).get('url', None),
+ "comment_message": self.replace_gitlab_references(project_url, comment_message),
+ }
- user = get_gitlab_user(None)
- if not all([comment_message, gitlab_url, project_url]):
- raise ActionSyntaxException(_("Invalid issue comment information"))
-
- issues = Issue.objects.filter(external_reference=["gitlab", gitlab_url])
- tasks = Task.objects.filter(external_reference=["gitlab", gitlab_url])
- uss = UserStory.objects.filter(external_reference=["gitlab", gitlab_url])
-
- for item in list(issues) + list(tasks) + list(uss):
- if number and subject and gitlab_user_name and gitlab_user_url:
- comment = _("Comment by [@{gitlab_user_name}]({gitlab_user_url} "
- "\"See @{gitlab_user_name}'s GitLab profile\") "
- "from GitLab.\nOrigin GitLab issue: [gl#{number} - {subject}]({gitlab_url} "
- "\"Go to 'gl#{number} - {subject}'\")\n\n"
- "{message}").format(gitlab_user_name=gitlab_user_name,
- gitlab_user_url=gitlab_user_url,
- number=number,
- subject=subject,
- gitlab_url=gitlab_url,
- message=comment_message)
- else:
- comment = _("Comment From GitLab:\n\n{message}").format(message=comment_message)
-
- snapshot = take_snapshot(item, comment=comment, user=user)
- send_notifications(item, history=snapshot)
+class PushEventHook(BaseGitLabEventHook, BasePushEventHook):
+ def get_data(self):
+ result = []
+ for commit in self.payload.get("commits", []):
+ user_name = commit.get('author', {}).get('name', None)
+ result.append({
+ "user_id": None,
+ "user_name": user_name,
+ "user_url": None,
+ "commit_id": commit.get("id", None),
+ "commit_url": commit.get("url", None),
+ "commit_message": commit.get("message").strip(),
+ })
+ return result
diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py
index cd4751fb..a31352ed 100644
--- a/taiga/hooks/gitlab/services.py
+++ b/taiga/hooks/gitlab/services.py
@@ -18,7 +18,6 @@
import uuid
-from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.conf import settings
@@ -41,18 +40,3 @@ def get_or_generate_config(project):
url = "{}?project={}&key={}".format(url, project.id, g_config["secret"])
g_config["webhooks_url"] = url
return g_config
-
-
-def get_gitlab_user(user_email):
- user = None
-
- if user_email:
- try:
- user = get_user_model().objects.get(email=user_email)
- except get_user_model().DoesNotExist:
- pass
-
- if user is None:
- user = get_user_model().objects.get(is_system=True, username__startswith="gitlab")
-
- return user
diff --git a/taiga/hooks/gogs/__init__.py b/taiga/hooks/gogs/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/hooks/gogs/api.py b/taiga/hooks/gogs/api.py
new file mode 100644
index 00000000..ced551de
--- /dev/null
+++ b/taiga/hooks/gogs/api.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.hooks.api import BaseWebhookApiViewSet
+
+from . import event_hooks
+
+
+class GogsViewSet(BaseWebhookApiViewSet):
+ event_hook_classes = {
+ "push": event_hooks.PushEventHook
+ }
+
+ def _validate_signature(self, project, request):
+ payload = self._get_payload(request)
+
+ if not hasattr(project, "modules_config"):
+ return False
+
+ if project.modules_config.config is None:
+ return False
+
+ secret = project.modules_config.config.get("gogs", {}).get("secret", None)
+ if secret is None:
+ return False
+
+ return payload.get('secret', None) == secret
+
+ def _get_event_name(self, request):
+ return "push"
diff --git a/taiga/hooks/gogs/event_hooks.py b/taiga/hooks/gogs/event_hooks.py
new file mode 100644
index 00000000..b392afe2
--- /dev/null
+++ b/taiga/hooks/gogs/event_hooks.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import re
+import os.path
+
+from taiga.hooks.event_hooks import BasePushEventHook
+
+
+class BaseGogsEventHook():
+ platform = "Gogs"
+ platform_slug = "gogs"
+
+ def replace_gogs_references(self, project_url, wiki_text):
+ if wiki_text is None:
+ wiki_text = ""
+
+ template = "\g<1>[Gogs#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
+ return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
+
+
+class PushEventHook(BaseGogsEventHook, BasePushEventHook):
+ def get_data(self):
+ result = []
+ commits = self.payload.get("commits", [])
+ project_url = self.payload.get("repository", {}).get("html_url", None)
+
+ for commit in filter(None, commits):
+ user_name = commit.get('author', {}).get('username', None)
+ result.append({
+ "user_id": user_name,
+ "user_name": user_name,
+ "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), user_name),
+ "commit_id": commit.get("id", None),
+ "commit_url": commit.get("url", None),
+ "commit_message": commit.get("message", None),
+ })
+ return result
diff --git a/taiga/hooks/gogs/migrations/0001_initial.py b/taiga/hooks/gogs/migrations/0001_initial.py
new file mode 100644
index 00000000..5c35b081
--- /dev/null
+++ b/taiga/hooks/gogs/migrations/0001_initial.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.core.files import File
+
+import uuid
+import os
+
+CUR_DIR = os.path.dirname(__file__)
+
+
+def create_gogs_system_user(apps, schema_editor):
+ # We get the model from the versioned app registry;
+ # if we directly import it, it'll be the wrong version
+ User = apps.get_model("users", "User")
+ db_alias = schema_editor.connection.alias
+
+ if not User.objects.using(db_alias).filter(is_system=True, username__startswith="gogs-").exists():
+ random_hash = uuid.uuid4().hex
+ user = User.objects.using(db_alias).create(
+ username="gogs-{}".format(random_hash),
+ email="gogs-{}@taiga.io".format(random_hash),
+ full_name="Gogs",
+ is_active=False,
+ is_system=True,
+ bio="",
+ )
+ f = open("{}/logo.png".format(CUR_DIR), "rb")
+ user.photo.save("logo.png", File(f))
+ user.save()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('users', '0010_auto_20150414_0936')
+ ]
+
+ operations = [
+ migrations.RunPython(create_gogs_system_user),
+ ]
diff --git a/taiga/hooks/gogs/migrations/__init__.py b/taiga/hooks/gogs/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/hooks/gogs/migrations/logo.png b/taiga/hooks/gogs/migrations/logo.png
new file mode 100644
index 00000000..384a58d2
Binary files /dev/null and b/taiga/hooks/gogs/migrations/logo.png differ
diff --git a/taiga/hooks/gogs/models.py b/taiga/hooks/gogs/models.py
new file mode 100644
index 00000000..fca83d73
--- /dev/null
+++ b/taiga/hooks/gogs/models.py
@@ -0,0 +1 @@
+# This file is needed to load migrations
diff --git a/taiga/hooks/gogs/services.py b/taiga/hooks/gogs/services.py
new file mode 100644
index 00000000..40d06fab
--- /dev/null
+++ b/taiga/hooks/gogs/services.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import uuid
+
+from django.core.urlresolvers import reverse
+
+from taiga.base.utils.urls import get_absolute_url
+
+
+# Set this in settings.PROJECT_MODULES_CONFIGURATORS["gogs"]
+def get_or_generate_config(project):
+ config = project.modules_config.config
+ if config and "gogs" in config:
+ g_config = project.modules_config.config["gogs"]
+ else:
+ g_config = {"secret": uuid.uuid4().hex}
+
+ url = reverse("gogs-hook-list")
+ url = get_absolute_url(url)
+ url = "%s?project=%s" % (url, project.id)
+ g_config["webhooks_url"] = url
+ return g_config
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index d643b4f8..e8e00410 100644
--- a/taiga/locale/ca/LC_MESSAGES/django.po
+++ b/taiga/locale/ca/LC_MESSAGES/django.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -20,150 +20,154 @@ msgstr ""
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "El registre públic està deshabilitat"
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "Sistema de registre invàlid"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "Sistema de login invàlid"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "El mot d'usuari ja està en ús."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Aquest e-mail ja està en ús."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "El token no s'ajusta a cap invitació vàlida"
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Aquest usuari ja està registrat"
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr ""
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Error creant un nou usuari."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Token invàlid"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "nom d'usuari invàlid"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Requerit. 255 caràcters o menys. Lletres, nombres i caràcters /./-/_"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "El mot d'usuari ja està en ús."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Aquest e-mail ja està en ús."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "El token no s'ajusta a cap invitació vàlida"
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Aquest usuari ja està registrat"
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr ""
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Error creant un nou usuari."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Token invàlid"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Aquest camp es obligatori"
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Valor invàlid"
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' valor deu ser Verdader o Fals"
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr "Introdueix un 'slug' vàlid: lletres, nombres, barra baixa o guió."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "Selecciona una opció vàlida. %(value)s no es una opció vàlida."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Introdueix una adreça de correu vàlida-"
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "La data te un format erroni. Utilitza un del següents formats: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "La data te un format erroni. Utilitza un del següents formats: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "L'hora te un format erroni. Utilitza un del següents formats: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Introdueix un nombre complet."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Asegurat que aquest valor es inferior i igual a %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Asegurat que aquest valor es superior o igual a %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" deu ser un float."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Introdueix un nombre."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Asegurat que no hi ha més de %s digits en total."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Asegurat que no hi ha més de %s posicions decimals."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Asegurat que no hi ha més de %s dígits abans del decimal."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Cap fitxer enviat. Comprova el tipus de codificació en el formulari."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Cap fitxer enviat."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "El fitxer enviat està buit."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -171,11 +175,11 @@ msgstr ""
"Asegurat que el nom del fitxer te un màxim de %(max)d caràcters (te "
"%(length)d)"
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "Per favor envia un fitxer o cancela el checkbox, pero no ambdós."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -183,180 +187,177 @@ msgstr ""
"Puja una imatge vàlida. El fitxer que has pujat no ès una imatge o el fitxer "
"està corrupte."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "La página no es 'last' ni pot ser convertida a un 'int'"
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Pàgina invàlida (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr ""
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr ""
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr ""
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr ""
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr ""
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr ""
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr ""
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr ""
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr ""
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr ""
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr ""
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr ""
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr ""
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr ""
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr ""
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Error de connexió."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr ""
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr ""
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr ""
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr ""
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr ""
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr ""
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr ""
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr ""
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr ""
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Error inesperat"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "No s'ha trobat."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Mètode no suportat per aquest endpoint."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Arguments invàlids."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Validació de data errònia"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Error d'integritat per argument invàlid o erroni."
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Precondició errònia."
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr ""
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr ""
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "tags"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -411,7 +412,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -468,103 +469,88 @@ msgstr ""
" Comentari: %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Es necessita arxiu dump."
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Format d'arxiu dump invàlid"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr ""
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Conté camps personalitzats invàlids."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr ""
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr ""
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr ""
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr ""
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr ""
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr ""
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr ""
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -584,15 +570,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr ""
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -743,77 +729,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] El teu bolcat de dades ha sigut importat"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr ""
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Conté camps personalitzats invàlids."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr ""
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr ""
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "Nom"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr ""
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "Descripció"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr ""
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr ""
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr ""
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "Nom complet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "Adreça d'email"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "Comentari"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "Data de creació"
@@ -844,7 +850,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informació extra"
@@ -878,504 +884,577 @@ msgstr ""
"\n"
"[Taiga] Feedback de %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "El payload no és un arxiu json vàlid"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "El projecte no existeix"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Firma no vàlida."
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "L'element referenciat no existeix"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "L'estatus no existeix."
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr ""
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Informació d'incidència no vàlida."
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Informació del comentari a l'incidència no vàlid."
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Informació d'incidència no vàlida."
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
+#, python-brace-format
+msgid ""
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"Changed status from {platform} commit.\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr ""
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr ""
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "L'element referenciat no existeix"
-#: taiga/hooks/github/event_hooks.py:201
-#, python-brace-format
-msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "L'estatus no existeix."
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Veure projecte"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Veure fita"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Veure història d'usuari"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Veure tasca"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Veure incidència"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Veure pàgina del wiki"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Veure links del wiki"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Demana membresía"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Afegeix història d'usuari a projecte"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Afegeix comentaris a històries d'usuari"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Afegeix comentaris a tasques"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Afegeix incidéncies"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Afegeix comentaris a incidéncies"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Afegeix pàgina del wiki"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Modifica pàgina del wiki"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Afegeix enllaç de wiki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modifica enllaç de wiki"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Afegeix fita"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modifica fita"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Borra fita"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Veure història d'usuari"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Afegeix història d'usuari"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Modifica història d'usuari"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Borra història d'usuari"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Afegeix tasca"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modifica tasca"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Borra tasca"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Afegeix incidència"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Modifica incidència"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Borra incidència"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Afegeix pàgina del wiki"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Modifica pàgina del wiki"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Borra pàgina de wiki"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Afegeix enllaç de wiki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modifica enllaç de wiki"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Borra enllaç de wiki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Modifica projecte"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Afegeix membre"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Borra membre"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Borra projecte"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Afegeix membre"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Borra membre"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Administrar valors de projecte"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Administrar rols"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "Amo"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Arguments incomplets."
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Format d'image invàlid"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "No tens permisos per a veure açò."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr ""
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "Projecte"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "Tipus de contingut"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "Id d'objecte"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "Data de modificació"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "Arxiu adjunt"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "està obsolet "
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "Ordre"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr ""
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr ""
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr ""
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
-msgid "Text"
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:29
-msgid "Date"
+msgid "Multi-Line Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:30
+msgid "Date"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "tipus"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "història d'usuari"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "tasca"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "incidéncia"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Ja existix altre amb el matex nom."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "estatus"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "tema"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "color"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "assignada a"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "requeriment de client"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "requeriment d'equip"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr ""
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr ""
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Canvia"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Crea"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Borra"
@@ -1431,7 +1510,7 @@ msgstr "Borrat"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Sense assignar"
@@ -1478,95 +1557,75 @@ msgstr "Desde:"
msgid "To:"
msgstr "A:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "contingut"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "nota de bloqueig"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "No tens permissos per a ficar aquest sprint a aquesta incidència"
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "No tens permissos per a ficar aquest status a aquesta tasca"
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "No tens permissos per a ficar aquesta severitat a aquesta tasca"
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "No tens permissos per a ficar aquesta prioritat a aquesta incidència"
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "No tens permissos per a ficar aquest tipus a aquesta incidència"
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "estatus"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "severitat"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "prioritat"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "fita"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "Data de finalització"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "tema"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "assignada a"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "referència externa"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "M'agrada"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Fans"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -1578,8 +1637,9 @@ msgstr "Data estimada d'inici"
msgid "estimated finish date"
msgstr "Data estimada de finalització"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "està tancat"
@@ -1591,290 +1651,384 @@ msgstr "disponibilitat"
msgid "The estimated start must be previous to the estimated finish."
msgstr ""
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "No hi ha cap sprint amb aquest id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "està bloquejat"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr ""
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "text extra d'invitació"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "L'usuari ja es membre del projecte"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "Points per defecte"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "estatus d'història d'usuai per defecte"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "Points per defecte"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "Estatus de tasca per defecte"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "Prioritat per defecte"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "Severitat per defecte"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "Status d'incidència per defecte"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "Tipus d'incidència per defecte"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total de fites"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "total de punts d'història"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "activa panell de backlog"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "activa panell de kanban"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "activa panell de wiki"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "activa panell d'incidències"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "sistema de videoconferència"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "template de creació"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "permisos d'anònims"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "permisos d'usuaris"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "es privat"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "permisos d'anònims"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "permisos d'usuaris"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "colors de tags"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "Actualitzada data"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "configuració de mòdules"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "està arxivat"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "color"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "limit de treball en progrés"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "rol d'amo per defecte"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "opcions per defecte"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "status d'històries d'usuari"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "punts"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "status de tasques"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "status d'incidències"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "tipus d'incidències"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "prioritats"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "severitats"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "rols"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr ""
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr ""
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "creada data"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr ""
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr ""
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr ""
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr ""
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr ""
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2352,159 +2506,179 @@ msgstr ""
"\n"
"[%(project)s] Borrada pàgina de Wiki \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr ""
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr ""
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr ""
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr ""
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "Versió"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Aquest e-mail ja està en ús"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Rol invàlid per al projecte"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Opcions per defecte"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Estatus d'històries d'usuari"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Punts"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Estatus de tasques"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Estatus d'incidéncies"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Tipus d'incidéncies"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Prioritats"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Severitats"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Rols"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr ""
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr ""
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Token invàlid"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "tags"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "colors de tags"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr ""
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "order d'històries d'usuari"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "ordre de taskboard"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "es iocaina"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "No hi ha cap tasca amb eixe id"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -2863,12 +3037,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr ""
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -2879,12 +3053,12 @@ msgid ""
msgstr ""
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr ""
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -2893,303 +3067,388 @@ msgid ""
msgstr ""
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr ""
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr ""
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr ""
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr ""
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr ""
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr ""
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr ""
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr ""
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr ""
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr ""
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr ""
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr ""
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr ""
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr ""
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr ""
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr ""
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr ""
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr ""
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr ""
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr ""
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr ""
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr ""
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr ""
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr ""
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr ""
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr ""
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr ""
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr ""
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rol"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "ordre de backlog"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "ordre d'sprint"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "data de finalització"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "requeriment de client"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "requeriment d'equip"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "generat desde incidéncia"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "No hi ha cap història d'usuari amb eixe id"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "No hi ha cap projecte amb eixe id"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "No hi ha cap estatis d'història d'usuari amb eixe id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Aquest e-mail ja està en ús"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "No hi ha cap estatus de tasca amb eixe id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Rol invàlid per al projecte"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Opcions per defecte"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Estatus d'històries d'usuari"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Punts"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Estatus de tasques"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Estatus d'incidéncies"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Tipus d'incidéncies"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioritats"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Severitats"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Rols"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Vots"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Vot"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr ""
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr ""
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "últim a modificar"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3217,53 +3476,53 @@ msgstr ""
msgid "Important dates"
msgstr "Dates importants"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Email duplicat"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Email no vàlid"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Nom d'usuari o email invàlid"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Correu enviat satisfactòriament"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Paràmetre de password actual requerit"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Paràmetre de password requerit"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Password invàlid, al menys 6 caràcters requerits"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Password actual invàlid"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Invàlid. Estás segur que el token es correcte i que no l'has usat abans?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Invàlid. Estás segur que el token es correcte?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "estatus de superusuari"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3271,24 +3530,24 @@ msgstr ""
"Designa que aquest usuari te tots els permisos sense asignarli-los "
"explícitament."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "mot d'usuari"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Requerit. 30 caràcters o menys. Lletres, nombres i caràcters /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Introdueix un nom d'usuari vàlid"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "actiu"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3296,71 +3555,63 @@ msgstr ""
"Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en "
"lloc de borrar el compte."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "data d'unió"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "llenguatge per defecte"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "zona horaria per defecte"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "coloritza tags"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "token de correu"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "nova adreça de correu"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "permissos"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "invàlid"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Nom d'usuari invàlid"
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr ""
@@ -3481,47 +3732,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr ""
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr ""
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "invàlid"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Nom d'usuari invàlid"
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr ""
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr ""
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr ""
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr ""
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr ""
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr ""
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr ""
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr ""
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 7b75e1f9..dd4e9ac4 100644
--- a/taiga/locale/de/LC_MESSAGES/django.po
+++ b/taiga/locale/de/LC_MESSAGES/django.po
@@ -13,12 +13,15 @@
# Sebastian Blum , 2015
# Silsha Fux , 2015
# Thomas McWork , 2015
+# Thomas Rößl , 2016
+# Tobias Klepp , 2016
+# Torsten Karge , 2016
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -28,170 +31,174 @@ msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Die Registrierung ist für die Öffentlichkeit gesperrrt."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "Ungültige Registrierungsart"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "Ungültige Loginart"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Der Benutzername wird schon verwendet."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Diese E-Mail Adresse wird schon verwendet."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Der Benutzer ist schon registriert."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "Dieser Benutzer ist schon ein Mitglied des Projektes."
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Fehler bei der Erstellung des neuen Benutzers."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Ungültiges Token"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "Ungültiger Benutzername"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
"255 oder weniger Zeichen aus Buchstaben, Zahlen und Punkt, Minus oder "
"Unterstrich erforderlich."
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Der Benutzername wird schon verwendet."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Diese E-Mail Adresse wird schon verwendet."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Der Benutzer ist schon registriert."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr ""
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Fehler bei der Erstellung des neuen Benutzers."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Ungültiges Token"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Das ist ein Pflichtfeld."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Ungültiger Wert."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "Der Wert für '%s' muss entweder True/Wahr oder False/Falsch sein."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Geben Sie einen gültigen 'slug' ein, bestehend aus Buchstaben, Zahlen, "
"Unterstrichen oder Bindestrichen."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "Bitte machen Sie eine gültige Auswahl. %(value)s ist nicht verfügbar."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Geben Sie bitte eine gültige E-Mail Adresse an."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr ""
"Das Datum hat das falsche Format. Bitte verwenden Sie eines der folgenden "
"Formate: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr ""
"Der Datentyp 'Datetime' hat ein falsches Format. Bitte verwenden Sie eines "
"der folgenden Formate: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr ""
"Die Zeit hat ein falsches Format. Bitte verwenden Sie eines der folgenden "
"Formate: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Geben Sie bitte eine ganze Zahl ein."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr ""
"Stellen Sie sicher, dass dieser Wert niedriger oder gleich ist wie "
"%(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr ""
"Stellen Sie sicher, dass dieser Wert höher oder gleich ist wie "
"%(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "Der Wert für '%s' muss eine Fließkommazahl sein."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Bitte geben Sie eine Zahl ein."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr ""
"Bitte stellen Sie sicher, dass nicht mehr als %s insgesamt vorhanden sind. "
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr ""
"Bitte stellen Sie sicher, dass nicht mehr als %s Dezimalstellen vorhanden "
"sind."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr ""
"Stellen Sie sicher, dass nicht mehr als %s Ziffern vor dem Dezimalpunkt "
"vorhanden sind."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr ""
"Es wurde keine Datei übergeben. Prüfen Sie die Kodierung der HTML-Form."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Es wurde keine Datei eingereicht."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Die eingereichte Datei ist leer."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -199,13 +206,13 @@ msgstr ""
"Stellen Sie sicher, dass dieser Dateiname höchstens %(max)d Zeichen hat (er "
"hat %(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Bitte senden Sie entweder eine Datei oder markieren Sie \"Löschen\", nicht "
"beides."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -213,182 +220,179 @@ msgstr ""
"Bitte laden Sie ein gültiges Bild hoch. Die Datei, die Sie hochgeladen "
"haben, ist entweder kein Bild oder defekt."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
-msgstr ""
+msgstr "Blockiertes Element"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Seite ist nicht 'letzte', noch kann diese konvertiert werden."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Ungültige Seite (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Ungültige Berechtigungsdefinition"
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Ungültige pk '%s' - Das Objekt existiert nicht."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Falsche Eingabe. Erwartet pk Wert, erhalten %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Objekt mit %s=%s existiert nicht."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Ungültiger Hyperlink - keine passende URL. "
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Ungültiger Hyperlink - Falsche URL Verknüpfung"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Ungültiger Hyperlink durch Konfigurationsfehler"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Ungültiger Hyperlink - Ziel existiert nicht."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Falsche Eingabe. Erwartet url Zeichenkette, erhalten %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Ungültige Daten"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Es gab keine Eingabe"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Es können nur existierende Einträge aktualisiert werden. Eine Neuerstellung "
"ist nicht möglich."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Es wurde eine Liste von Einträgen erwartet."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Nicht gefunden."
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Zugriff verweigert"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Fehler bei der Serveranmeldung"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Verbindungsfehler."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Fehlerhafte Anfrage."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Ungültige Authentifizierungsdaten."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Die Authentifizierungsdaten wurden nicht erbracht."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Sie haben keine Berechtigung, diese Aktion auszuführen. "
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Methode '%s' ist nicht erlaubt."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Könnte der Anforderung im Header nicht entsprechen."
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Nicht unterstützter Medientyp '%s' in Anfrage."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Die Anfrage wurde ausgebremst."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Voraussichtlich verfügbar in %d second%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Unerwarteter Fehler"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Nicht gefunden."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Methode wird für diesen Endpunkt nicht unterstützt. "
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Falsche Argumente"
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Fehler bei Datenüberprüfung "
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Voraussetzungsfehler"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
-msgstr ""
+msgstr "Kein Raum für weitere Projekte."
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Fehler in Filter Parameter Typen."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' muss ein Integer-Wert sein."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "Tags"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -443,7 +447,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -455,22 +459,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" Taiga Support:\n"
-" "
-"%(support_url)s\n"
-"
\n"
-" Kontaktieren Sie uns:\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Mailing list:\n"
-" \n"
-" %(mailing_list_url)s\n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -523,103 +511,88 @@ msgstr ""
"Kommentar: %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Es ist mindestens eine Rolle nötig"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Exportdatei erforderlich"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Ungültiges Exportdatei Format"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Enthält ungültige Benutzerfelder."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Der Name für das Projekt ist doppelt vergeben"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "Fehler beim Importieren der Projektdaten"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "Fehler beim Importieren der Rollen"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "Fehler beim Importieren der Mitgliedschaften"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "Fehler beim Importieren der Listen von Projektattributen"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte "
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "Fehler beim Importieren der Kundenattribute"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "Fehler beim Import der Sprints"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "Fehler beim Importieren der User-Stories"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "Fehler beim Importieren der Aufgaben"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "Fehler beim Importieren der Tickets"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "Fehler beim Importieren der User-Stories"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "Fehler beim Importieren der Aufgaben"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "Fehler beim Importieren von Wiki Seiten"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "Fehler beim Importieren von Wiki Links"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "Fehler beim Importieren der Schlagworte"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "Fehler beim Importieren der Chroniken"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
-msgstr ""
+msgstr "unerwarteter Fehler beim Projekt-Import"
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Fehler beim Erzeugen der Projekt Export-Datei "
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -638,18 +611,33 @@ msgid ""
"TRACE ERROR:\n"
"------------"
msgstr ""
+"\n"
+"\n"
+"Fehler beim Laden des Dump von {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"GRUND:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"FEHLER-PFAD:\n"
+"------------"
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Fehler beim Laden von Projekt Export-Datei"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
-msgstr ""
+msgstr "Fehler beim Laden Ihrer Projekt-Dump-Datei"
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
-msgstr ""
+msgstr "-- keine detaillierten Infos --"
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
@@ -891,77 +879,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Ihre Projekt Export-Datei wurde importiert"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Enthält ungültige Benutzerfelder."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Der Name für das Projekt ist doppelt vergeben"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Authentifizierung erforderlich"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "Name"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "Icon URL"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "Web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "Beschreibung"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Nächste URL"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "Geheimer Schlüssel für Verschlüsselung der Anwensungs-Token"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "Benutzer"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "Applikation"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "vollständiger Name"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "E-Mail Adresse"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "Kommentar"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "Erstellungsdatum"
@@ -991,7 +999,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Zusätzliche Information"
@@ -1026,546 +1034,579 @@ msgstr ""
"[Taiga] Feedback von %(full_name)s <%(email)s>\n"
" \n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "Die Nutzlast ist kein gültiges json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Das Projekt existiert nicht"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Falsche Signatur"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Das referenzierte Element existiert nicht"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Der Status existiert nicht"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Der Status des BitBucket Commits hat sich geändert"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Ungültige Ticket-Information"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-"Problem-Bericht erstellt von [@{bitbucket_user_name}]({bitbucket_user_url} "
-"\"Schau @{bitbucket_user_name}'s BitBucket profile\") von BitBucket.\n"
-"Original BitBucket Problem-Bericht: [bb#{number} - {subject}]"
-"({bitbucket_url} \"Gehe zu 'bb#{number} - {subject}'\"):\n"
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
"\n"
-"{description}"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Ticket erstellt von BitBucket."
-
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Ungültige Ticket-Kommentar Information"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-"Kommentar von [@{bitbucket_user_name}]({bitbucket_user_url} \"Schau "
-"@{bitbucket_user_name}'s BitBucket profile\") von BitBucket.\n"
-"Original BitBucket Problem-Bericht: [bb#{number} - {subject}]"
-"({bitbucket_url} \"Gehe zu 'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Ungültige Ticket-Information"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Kommentar von BitBucket\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Status geändert von [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Der Status des GitHub Commits hat sich geändert"
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"Ticket erstellt von [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-" Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-" {description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Ticket erstellt von GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-"Kommentar von [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") von GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Kommentar von GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Das referenzierte Element existiert nicht"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Der Status des GitLab Commits hat sich geändert"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Der Status existiert nicht"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Erstellt von GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Kommentar von [@{gitlab_user_name}]({gitlab_user_url} \"Schau "
-"@{gitlab_user_name}'s GitLab profile\") von GitLab.\n"
-"Original GitLab Problem-Bericht: [gl#{number} - {subject}]({gitlab_url} "
-"\"Gehe zu 'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Kommentar von GitLab:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Projekt ansehen"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Meilensteine ansehen"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "User-Stories ansehen. "
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Aufgaben ansehen"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Tickets ansehen"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Wiki Seiten ansehen"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Wiki Links ansehen"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Mitgliedschaft beantragen"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "User-Story zu Projekt hinzufügen"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Kommentar zu User-Stories hinzufügen"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Kommentare zu Aufgaben hinzufügen"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Tickets hinzufügen"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Kommentare zu Tickets hinzufügen"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Wiki Seite hinzufügen"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Wiki Seite ändern"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Wiki Link hinzufügen"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Wiki Link ändern"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Meilenstein hinzufügen"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Meilenstein ändern"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Meilenstein löschen"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "User-Story ansehen"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "User-Story hinzufügen"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "User-Story ändern"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "User-Story löschen"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Aufgabe hinzufügen"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Aufgabe ändern"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Aufgabe löschen"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Ticket hinzufügen"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Ticket ändern"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Gelöschtes Ticket"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Wiki Seite hinzufügen"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Wiki Seite ändern"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Wiki Seite löschen"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Wiki Link hinzufügen"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Wiki Link ändern"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Wiki Link löschen"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Projekt ändern"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Mitglied hinzufügen"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Mitglied entfernen"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Projekt löschen"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Mitglied hinzufügen"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Mitglied entfernen"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Administrator Projekt Werte"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Administrator-Rollen"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "Besitzer"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Unvollständige Argumente"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Ungültiges Bildformat"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Unglültiger Templatename"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Ungültige Templatebeschreibung"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
-msgstr ""
+msgstr "Ungültige Benutzer-Id"
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
-msgstr ""
+msgstr "Der Benutzer existiert nicht"
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
-msgstr ""
+msgstr "Der Benutzer muss bereits Mitglied des Projektes sein"
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
+"Das Projekt muss einen Eigentümer haben und mindestens ein Benutzer muss ein "
+"aktiver Administrator sein"
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Sie haben keine Berechtigungen für diese Ansicht"
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Teil-Aktualisierungen sind nicht unterstützt"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "Projekt"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "Inhaltsart"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "Objekt Nr."
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "Zeitpunkt der Änderung"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "Angehangene Datei"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "SHA1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "wurde verworfen"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "Reihenfolge"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "Erscheint in"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Kunde"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Gesprächig"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
-msgstr ""
+msgstr "Dieses Projekt ist durch den Administrator blockiert"
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
+msgstr "Dieses Projekt ist blockiert, weil es der Eigentümer verlassen hat."
+
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Text"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Mehrzeiliger Text"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Datum"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
-msgstr ""
+msgstr "Url"
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "Art"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "Werte"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "User-Story"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "Aufgabe"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "Ticket"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Dieser Name wird schon verwendet."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "Status"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "Betreff"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "Farbe"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "zugewiesen an"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "ist Kundenanforderung"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "ist Teamanforderung"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Kommentar bereits gelöscht"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Kommentar nicht gelöscht"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Ändern"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Erzeugen"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Löschen"
@@ -1621,7 +1662,7 @@ msgstr "entfernt"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Nicht zugewiesen"
@@ -1668,99 +1709,79 @@ msgstr "Von:"
msgid "To:"
msgstr "An:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "Inhalt"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "Blockierungsgrund"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "Sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diesen Sprint zu setzen."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diesen Status zu setzen. "
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diese Gewichtung zu setzen."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"Sie haben nicht die Berechtigung, das Ticket auf diese Priorität zu setzen. "
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Sie haben nicht die Berechtigung, das Ticket auf diese Art zu setzen."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "Status"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "Gewichtung"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "Priorität"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "Meilenstein"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "Datum der Fertigstellung"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "Betreff"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "zugewiesen an"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "externe Referenz"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Like"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Likes"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "Slug"
@@ -1772,8 +1793,9 @@ msgstr "geschätzter Starttermin"
msgid "estimated finish date"
msgstr "geschätzter Endtermin"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "ist geschlossen"
@@ -1785,290 +1807,384 @@ msgstr "Verfügbarkeit"
msgid "The estimated start must be previous to the estimated finish."
msgstr "Der erwartete Beginn muss vor dem erwarteten Ende liegen. "
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Es gibt keinen Sprint mit dieser id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "wird blockiert"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' Parameter ist ein Pflichtfeld"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "Der 'project' Parameter ist ein Pflichtfeld"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "E-Mail"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "erstellt am "
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "Token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "Einladung Zusatztext "
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "Benutzerreihenfolge"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "Der Benutzer ist bereits Mitglied dieses Projekts"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "voreingestellte Punkte"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "voreingesteller User-Story Status "
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "voreingestellte Punkte"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "voreingestellter Aufgabenstatus"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "voreingestellte Priorität "
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "voreingestellte Gewichtung "
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "voreingestellter Ticket Status"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "voreingestellter Ticket Typ"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
-msgstr ""
+msgstr "Logo"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "Mitglieder"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "Meilensteine Gesamt"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "Story Punkte insgesamt"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "aktives Backlog Panel"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "aktives Kanban Panel"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "aktives Wiki Panel"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "aktives Tickets Panel"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "Videokonferenzsystem"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "Zusatzdaten Videokonferenz"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "Vorlage erstellen"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "Rechte für anonyme Nutzer"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "Rechte für registrierte Nutzer"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "ist privat"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "Rechte für anonyme Nutzer"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "Rechte für registrierte Nutzer"
+
+#: taiga/projects/models.py:199
msgid "is featured"
-msgstr ""
+msgstr "ist gekennzeichnet"
+
+#: taiga/projects/models.py:202
+msgid "is looking for people"
+msgstr "sucht nach Mitarbeitern"
#: taiga/projects/models.py:204
-msgid "is looking for people"
-msgstr ""
-
-#: taiga/projects/models.py:206
msgid "loking for people note"
-msgstr ""
+msgstr "Hinweis für Mitarbeitersuche"
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "Tag Farben"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
-msgstr ""
+msgstr "Projekt-Transfer-Token"
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
-msgstr ""
+msgstr "Blockierter Code"
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "Aktualisierungsdatum"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "Count"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
-msgstr ""
+msgstr "Unterstützer letzte Woche"
+
+#: taiga/projects/models.py:235
+msgid "fans last month"
+msgstr "Unterstützer letzten Monat"
#: taiga/projects/models.py:238
-msgid "fans last month"
-msgstr ""
-
-#: taiga/projects/models.py:241
msgid "fans last year"
-msgstr ""
+msgstr "Unterstützer letztes Jahr"
+
+#: taiga/projects/models.py:244
+msgid "activity last week"
+msgstr "Aktivitäten letzte Woche"
#: taiga/projects/models.py:247
-msgid "activity last week"
-msgstr ""
+msgid "activity last month"
+msgstr "Aktivitäten letzten Monat"
#: taiga/projects/models.py:250
-msgid "activity last month"
-msgstr ""
-
-#: taiga/projects/models.py:253
msgid "activity last year"
-msgstr ""
+msgstr "Aktivitäten letztes Jahr"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "Module konfigurieren"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "ist archiviert"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "Farbe"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "Ausführungslimit"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "Wert"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "voreingestellte Besitzerrolle"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "Vorgabe Optionen"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "User-Story Status "
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "Punkte"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "Ticket Status"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "Ticket Arten"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "Prioritäten"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "Gewichtung"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "Rollen"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Beteiligt"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Alle"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Keine"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "Erstelldatum"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "Chronik Einträge"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "Benutzer benachrichtigen"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Beobachtet"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Benachrichtigung für bestimmte Benutzer und Projekt aktiviert"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Ungültiger Wert für Benachrichtigungslevel"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2824,163 +2940,189 @@ msgstr ""
"[%(project)s] löschte die Wiki Seite \"%(page)s\"\n"
"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Beobachter enthält ungültige Benutzer "
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "Die Watcher beinhalten einen ungültigen Benutzer"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "Der Versionsparameter ist ungültig"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Die Version stimmt nicht mit der aktuellen überein"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "Version"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
+"Sie können das Projekt nicht verlassen, wenn Sie der Eigentümer sind oder "
+"wenn keine weiteren Administratoren vorhanden sind."
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Die E-Mailadresse ist bereits vergeben"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Ungültige Rolle für dieses Projekt"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Voreingestellte Optionen"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Status für User-Stories"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Punkte"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Aufgaben Status"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Ticket Status"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Ticket Arten"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Prioritäten"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Gewichtung"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Rollen"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
+"Sie haben Ihr aktuelles Limit für die Mitgliederanzahl für private Projekte "
+"erreicht"
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
+"Sie haben Ihr aktuelles Limit für die Mitgliederanzahl für öffentliche "
+"Projekte erreicht"
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
-msgstr ""
+msgstr "Sie können nicht mehr private Projekte haben"
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
-msgstr ""
+msgstr "Sie können nicht mehr öffentliche Projekte haben."
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Zukünftiger Sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Projektende"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Token ist ungültig"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
+msgstr "Token ist abgelaufen"
+
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "Tags"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "Tag Farben"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Sprint auf diese Aufgabe zu setzen"
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diese User-Story auf diese Aufgabe zu "
"setzen"
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Status auf diese Aufgabe zu setzen."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "User-Story Befehl "
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "Taskboard Befehl "
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "ist Iocaine"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Es gibt keine Aufgabe mit dieser id"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3175,7 +3317,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
-msgstr ""
+msgstr "%(new_owner_name)s sagt:"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
@@ -3191,6 +3333,8 @@ msgid ""
"\n"
"The Taiga Team\n"
msgstr ""
+"\n"
+"Das Taiga-Team\n"
#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
#, python-format
@@ -3283,7 +3427,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
msgid "Continue"
-msgstr ""
+msgstr "Fortsetzen"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
#, python-format
@@ -3303,7 +3447,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
msgid "Go to your project settings:"
-msgstr ""
+msgstr "Geben Sie zu Ihren Projekt-Einstellungen:"
#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
#, python-format
@@ -3329,6 +3473,8 @@ msgid ""
" %(owner_name)s says:
\n"
" "
msgstr ""
+"\n"
+"%(owner_name)s sagt:
"
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
@@ -3371,12 +3517,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3393,12 +3539,12 @@ msgstr ""
"seiner Kunden gerecht wird."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3412,317 +3558,403 @@ msgstr ""
"der nächsten Integrationsstufe verbaut werden."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Neu"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Fertig"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "In Arbeit"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Bereit zum Testen"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Erledigt"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Archiviert"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Geschlossen"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Information wird benötigt"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Verschoben"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Zurückgewiesen"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Fehler"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Frage"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Erweiterung"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Niedrig"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Hoch"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Wunschliste"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Gering"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Wichtig"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Kritisch"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Projekteigentümer "
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Sprint auf diese User-Story zu "
"setzen."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Status auf diese User-Story zu "
"setzen."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Erstelle die User-Story #{ref} - {subject}"
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "Rolle"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "Backlog Befehl "
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "Sprintreihenfolge"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "Endtermin"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "ist Kundenanforderung"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "ist Teamanforderung"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "erzeugt von Ticket"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Es gibt keine User-Story mit dieser id"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Es gibt kein Projekt mit dieser id"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Es gibt keinen User-Story Status mit dieser id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Die E-Mailadresse ist bereits vergeben"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Es gibt keinen Aufgabenstatus mit dieser id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Ungültige Rolle für dieses Projekt"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr "Der Projekteigentümer muss Administrator sein."
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+"Mindestens ein Benutzer muss ein aktiver Administrator des Projektes sein."
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Voreingestellte Optionen"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Status für User-Stories"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Punkte"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Aufgaben Status"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Ticket Status"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Ticket Arten"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioritäten"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Gewichtung"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Rollen"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Stimmen"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Stimme"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'content' Parameter ist erforderlich"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' Parameter ist erforderlich"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "letzte Änderung"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Prüfe die API der Historie auf Übereinstimmung"
-#: taiga/users/admin.py:38
-msgid "Project Member"
-msgstr ""
-
#: taiga/users/admin.py:39
-msgid "Project Members"
-msgstr ""
+msgid "Project Member"
+msgstr "Projektmitglied"
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:40
+msgid "Project Members"
+msgstr "Projektmitglieder"
+
+#: taiga/users/admin.py:50
msgid "id"
-msgstr ""
+msgstr "Id"
#: taiga/users/admin.py:81
msgid "Project Ownership"
-msgstr ""
+msgstr "Projekt-Besitz"
#: taiga/users/admin.py:82
msgid "Project Ownerships"
-msgstr ""
+msgstr "Projekt-Besitze"
#: taiga/users/admin.py:119
msgid "Personal info"
@@ -3734,60 +3966,60 @@ msgstr "Berechtigungen"
#: taiga/users/admin.py:123
msgid "Restrictions"
-msgstr ""
+msgstr "Einschränkungen"
#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Wichtige Termine"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Doppelte E-Mail"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Ungültige E-Mail"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Ungültiger Benutzername oder E-Mail"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "E-Mail erfolgreich gesendet."
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Aktueller Passwort Parameter wird benötigt"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Neuer Passwort Parameter benötigt"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Ungültige Passwortlänge, mindestens 6 Zeichen erforderlich"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Ungültiges aktuelles Passwort"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Ungültig. Sind Sie sicher, dass das Token korrekt ist und Sie es nicht "
"bereits verwendet haben?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Ungültig. Sind Sie sicher, dass das Token korrekt ist?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "Superuser Status"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3795,25 +4027,25 @@ msgstr ""
"Dieser Benutzer soll alle Berechtigungen erhalten, ohne dass diese zuvor "
"zugewiesen werden müssen. "
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "Benutzername"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Benötigt. 30 Zeichen oder weniger.. Buchstaben, Zahlen und /./-/_ Zeichen"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Geben Sie einen gültigen Benuzternamen ein."
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3821,71 +4053,63 @@ msgstr ""
"Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen "
"Benutzer zu löschen."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "Über mich"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "Foto"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "Beitrittsdatum"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "Vorgegebene Sprache"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "Standard-Theme"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "Vorgegebene Zeitzone"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "Tag-Farben"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "E-Mail Token"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "neue E-Mail Adresse"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "Berechtigungen"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "ungültig"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein."
@@ -4087,49 +4311,53 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Sie wurden taigatisiert! "
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Es gibt keine Rolle mit dieser id"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "ungültig"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
"Doppelter Schlüsselwert verstößt einzigartige Vorgaben. Schlüssel '{}' "
"existiert bereits."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "Schlüssel"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "Geheimer Schlüssel"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "Status Code"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "Anfrage Daten"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "Anfrage Header"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "Antwort Daten"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "Antwort Header"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "Dauer"
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 4cde7d23..674cde40 100644
--- a/taiga/locale/en/LC_MESSAGES/django.po
+++ b/taiga/locale/en/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -16,339 +16,340 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr ""
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr ""
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr ""
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr ""
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr ""
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr ""
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr ""
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr ""
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr ""
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr ""
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr ""
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr ""
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr ""
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr ""
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr ""
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr ""
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr ""
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr ""
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr ""
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr ""
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr ""
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr ""
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr ""
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr ""
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr ""
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr ""
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr ""
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr ""
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr ""
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr ""
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr ""
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr ""
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr ""
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr ""
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr ""
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr ""
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
msgstr ""
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
msgstr ""
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr ""
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr ""
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr ""
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr ""
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr ""
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr ""
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr ""
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr ""
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr ""
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr ""
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr ""
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr ""
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr ""
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr ""
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr ""
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr ""
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr ""
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr ""
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr ""
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr ""
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr ""
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr ""
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr ""
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr ""
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr ""
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr ""
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr ""
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr ""
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr ""
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr ""
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr ""
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr ""
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr ""
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr ""
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr ""
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr ""
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr ""
@@ -403,7 +404,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -457,103 +458,88 @@ msgid ""
" "
msgstr ""
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr ""
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr ""
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr ""
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr ""
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr ""
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr ""
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr ""
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr ""
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr ""
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr ""
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr ""
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr ""
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -573,15 +559,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr ""
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -732,77 +718,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr ""
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr ""
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr ""
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr ""
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr ""
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr ""
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr ""
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr ""
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr ""
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr ""
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr ""
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr ""
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr ""
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr ""
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr ""
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr ""
@@ -825,7 +831,7 @@ msgid ""
msgstr ""
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr ""
@@ -851,504 +857,577 @@ msgid ""
"[Taiga] Feedback from %(full_name)s <%(email)s>\n"
msgstr ""
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr ""
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr ""
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr ""
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr ""
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr ""
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr ""
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
+#, python-brace-format
+msgid ""
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"Changed status from {platform} commit.\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr ""
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:201
-#, python-brace-format
-msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr ""
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr ""
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr ""
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr ""
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr ""
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr ""
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr ""
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr ""
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr ""
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr ""
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr ""
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr ""
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr ""
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr ""
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr ""
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr ""
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr ""
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr ""
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr ""
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr ""
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr ""
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr ""
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr ""
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr ""
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr ""
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr ""
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr ""
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr ""
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr ""
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr ""
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr ""
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr ""
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr ""
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr ""
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr ""
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr ""
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr ""
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr ""
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr ""
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr ""
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr ""
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr ""
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr ""
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr ""
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr ""
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr ""
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr ""
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr ""
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr ""
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr ""
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr ""
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr ""
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr ""
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr ""
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr ""
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr ""
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr ""
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr ""
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr ""
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr ""
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
-msgid "Text"
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:29
-msgid "Date"
+msgid "Multi-Line Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:30
+msgid "Date"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr ""
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr ""
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr ""
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr ""
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr ""
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr ""
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr ""
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr ""
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr ""
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr ""
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr ""
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr ""
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr ""
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr ""
@@ -1404,7 +1483,7 @@ msgstr ""
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr ""
@@ -1451,95 +1530,75 @@ msgstr ""
msgid "To:"
msgstr ""
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr ""
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr ""
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr ""
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr ""
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr ""
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr ""
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr ""
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr ""
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr ""
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr ""
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr ""
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr ""
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr ""
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr ""
@@ -1551,8 +1610,9 @@ msgstr ""
msgid "estimated finish date"
msgstr ""
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr ""
@@ -1564,290 +1624,384 @@ msgstr ""
msgid "The estimated start must be previous to the estimated finish."
msgstr ""
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr ""
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr ""
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr ""
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr ""
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr ""
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr ""
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr ""
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr ""
-#: taiga/projects/models.py:116
-msgid "default points"
+#: taiga/projects/models.py:112
+msgid "default epic status"
msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr ""
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr ""
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr ""
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr ""
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr ""
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr ""
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr ""
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr ""
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr ""
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr ""
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr ""
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr ""
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr ""
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr ""
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr ""
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr ""
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr ""
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr ""
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr ""
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr ""
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr ""
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr ""
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr ""
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr ""
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr ""
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr ""
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr ""
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr ""
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr ""
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr ""
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr ""
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr ""
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr ""
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr ""
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr ""
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr ""
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr ""
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr ""
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr ""
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr ""
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr ""
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr ""
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr ""
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr ""
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr ""
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr ""
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2319,158 +2473,178 @@ msgid ""
"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n"
msgstr ""
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr ""
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr ""
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr ""
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr ""
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr ""
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr ""
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
-msgstr ""
-
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr ""
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr ""
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr ""
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr ""
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr ""
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr ""
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr ""
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr ""
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr ""
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr ""
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr ""
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr ""
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr ""
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr ""
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr ""
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr ""
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr ""
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
@@ -2812,12 +2986,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr ""
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -2828,12 +3002,12 @@ msgid ""
msgstr ""
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr ""
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -2842,303 +3016,388 @@ msgid ""
msgstr ""
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr ""
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr ""
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr ""
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr ""
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr ""
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr ""
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr ""
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr ""
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr ""
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr ""
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr ""
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr ""
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr ""
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr ""
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr ""
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr ""
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr ""
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr ""
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr ""
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr ""
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr ""
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr ""
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr ""
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr ""
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr ""
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr ""
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr ""
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr ""
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr ""
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr ""
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr ""
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr ""
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr ""
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr ""
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr ""
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr ""
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr ""
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr ""
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
msgstr ""
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
msgstr ""
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr ""
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr ""
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr ""
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr ""
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr ""
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr ""
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr ""
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr ""
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr ""
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr ""
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr ""
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr ""
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr ""
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr ""
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr ""
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3166,145 +3425,137 @@ msgstr ""
msgid "Important dates"
msgstr ""
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr ""
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr ""
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr ""
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr ""
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr ""
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr ""
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr ""
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr ""
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr ""
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr ""
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr ""
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr ""
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr ""
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr ""
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr ""
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr ""
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr ""
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr ""
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr ""
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr ""
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr ""
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr ""
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr ""
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr ""
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr ""
@@ -3425,47 +3676,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr ""
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
+#: taiga/users/validators.py:45
+msgid "invalid"
msgstr ""
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr ""
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr ""
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr ""
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr ""
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr ""
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr ""
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr ""
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr ""
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr ""
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index ec287179..7340cd07 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -5,9 +5,10 @@
# Translators:
# David Barragán , 2015-2016
# Esther Moreno , 2015
-# gustavodiazjaimes , 2015
+# Gustavo Díaz Jaimes , 2015
# Hector Colina , 2015
# Jesus Marin , 2015
+# Jorge Sanchez , 2016
# Luis Sebastian Urrutia Fuentes , 2016
# Renelis Abreu Ramirez , 2016
# Taiga Dev Team , 2015-2016
@@ -16,8 +17,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@@ -27,162 +28,166 @@ msgstr ""
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "El registro público está deshabilitado."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "Tipo de registro inválido"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "Tipo de login inválido"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Nombre de usuario no disponible"
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Email no disponible"
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "El token no pertenece a ninguna invitación válida."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Este usuario ya está registrado."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "Este usuario ya es miembro del proyecto."
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Error al crear un nuevo usuario "
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Token inválido"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "nombre de usuario no válido"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Son necesarios. 255 caracteres o menos (letras, números y /./-/_)"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Nombre de usuario no disponible"
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Email no disponible"
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "El token no pertenece a ninguna invitación válida."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Este usuario ya está registrado."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "Este usuario ya es miembro del proyecto."
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Error al crear un nuevo usuario "
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Token inválido"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Este campo es requerido."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Valor inválido."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "El valor para '%s' debe ser True o False."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Escribe un slug válido que esté formado por letras, números o los símbolos "
"de guión o subrayado."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Seleccione una opción válida. %(value)s no es una de las opciones "
"disponibles."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Introduzca una dirección de email válida."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr ""
"La fecha posee un formato inválido. Utiliza alguno de los siguientes "
"formatos: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr ""
"La fecha y hora poseen un formato inválido. Utiliza alguno de los siguientes "
"formatos: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr ""
"El tiempo indicado posee un formato inválido. Utiliza alguno de los "
"siguientes formatos: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Introduce un número entero"
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Asegúrate de que el valor es menor o igual a %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Asegúrate de que el valor es mayor o igual a %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "El valor \"%s\" debe ser un número en coma flotante."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Introduce un número."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Asegúrate de que no haya más de %s dígitos en total."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Asegúrate de que no haya más de %s decimales."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr ""
"Asegúrate de que no haya más de %s dígitos en la parte entera del número."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr ""
"No se ha adjuntado ningún archivo. Comprueba el encoding en el formulario."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "No se envió el archivo"
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "El archivo enviado está vacío."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -190,193 +195,190 @@ msgstr ""
"Asegúrate de que el nombre del fichero contiene menos de %(max)d caracteres "
"(ahora tiene %(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "Por favor, adjunta un fichero o marca la casilla de vacío, no ambos."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Elemento bloqueado"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "La página no es 'last' o no es un número."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Página no válida (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Definición de permiso inválida."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "PK '%s' inválida - el objeto no existe."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr ""
"Tipo incorrecto. Se esperaba un identificador (pk) y se ha recibido %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "El objeto con %s=%s no existe."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Hipervínculo inválido - La URL no encaja con ningun objeto."
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Hipervínculo inválido - La URL es incorrecta"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Hipervínculo inválido debido a un error de configuración"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Hipervínculo inválido - el objeto no existe."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Tipo incorrecto. Se esperaba una url y se ha recibido %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Datos invalidos"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "No se han introducido datos."
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"No se pueden crear nuevos objetos. Sólo está permitida la actualización de "
"los existentes."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Se esperaba una lista de objetos."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "No encontrado"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Permiso denegado."
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Error en la aplicación del servidor."
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Error de conexión"
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Petición con formato incorrecto."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Credenciales de autenticación incorrectas."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "No se han proporcionado las credenciales de autenticación."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "No tienes permisos para realizar esta acción."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Método '%s' no permitido."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "No se ha podido satisfacer la perición de cabecera Accept"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Típo de medio '%s' no soportado."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Demasiadas peticiones."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Estará disponible en %d segundos%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Error inesperado"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "No encontrado."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Método no soportado por este recurso."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Argumentos erróneos."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Error de validación de datos"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Error de integridad por argumentos incorrectos o inválidos"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Error por incumplimiento de precondición"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
-msgstr ""
+msgstr "No hay espacio para mas proyectos"
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Error en los típos de parámetros de filtrado"
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' debe ser un valor entero."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "etiquetas"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -431,7 +433,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -443,22 +445,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-"Soporte de Taiga:\n"
-"%(support_url)s\n"
-"
\n"
-"Contáctanos:\n"
-"\n"
-"%(support_email)s\n"
-"\n"
-"
\n"
-"Lista de correo:\n"
-"\n"
-"%(mailing_list_url)s\n"
-""
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -510,103 +496,88 @@ msgstr ""
"\n"
"Comentario: %(comment)s"
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Necesitamos al menos un rol"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Se necesita el fichero con los datos exportados"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Formato de fichero de exportación inválido"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" no se ha encontrado en este proyecto"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Contiene attributos personalizados inválidos."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Nombre duplicado para el proyecto"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "error importando los datos del proyecto"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "error importando los roles"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "error importando los miembros"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "error importando la listados de valores de attributos del proyecto"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "error importando los valores por defecto de los atributos del proyecto"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "error importando los atributos personalizados"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "error importando los sprints"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "error importando las historias de usuario"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "error importando las tareas"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "error importando las peticiones"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "error importando las historias de usuario"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "error importando las tareas"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "error importando las páginas del wiki"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "error importando los enlaces del wiki"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "error importando las etiquetas"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "error importando los timelines"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
-msgstr ""
+msgstr "Error inesperado al importar el proyecto"
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Erro generando el volcado de datos del proyecto"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -625,18 +596,33 @@ msgid ""
"TRACE ERROR:\n"
"------------"
msgstr ""
+"\n"
+"\n"
+"Error cargando importacion {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Error cargando el volcado de datos del proyecto"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
-msgstr ""
+msgstr "Error cargando el archivo del proyecto exportado"
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
-msgstr ""
+msgstr "-- sin informacion --"
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
@@ -873,77 +859,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Tu proyecto ha sido importado"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" no se ha encontrado en este proyecto"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Contiene attributos personalizados inválidos."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Nombre duplicado para el proyecto"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Se requiere autenticación"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "nombre"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "URL del icono"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "descripción"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Siguiente URL"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "clave secreta para cifrar los tokens de aplicación"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "usuario"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "aplicación"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "nombre completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "dirección de email"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "comentario"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "fecha de creación"
@@ -973,7 +979,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Información extra"
@@ -1007,388 +1013,345 @@ msgstr ""
"\n"
"[Taiga] Feedback de %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "El payload no es un json válido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "El proyecto no existe"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Firma errónea"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "El elemento referenciado no existe"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "El estado no existe"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Estado cambiado desde un commit de BitBucket"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Información inválida de Issue"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-"Petición creada por [@{bitbucket_user_name}]({bitbucket_user_url} \"Ver el "
-"perfil de @{bitbucket_user_name} en BitBucket\") desde BitBucket.\n"
-"Petición de origen en BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Ir a 'bb#{number} - {subject}'\"):\n"
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
"\n"
-"{description}"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Petición creada desde BitBucket."
-
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Información de comentario de Issue inválida"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-"Comentario de [@{bitbucket_user_name}]({bitbucket_user_url} \"\"Ver el "
-"perfil de @{bitbucket_user_name} en BitBucket\") desde BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Información inválida de Issue"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Comentario desde BitBucket:\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"[@{github_user_name}]({github_user_url} \"Ver el perfil de "
-"@{github_user_name} en GitHub\") ha cambiado el estado a través del commit "
-"de GitHub [{commit_id}]({commit_url} \"Ver commit '{commit_id} - "
-"{commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Estado cambiado a través de un commit en GitHub."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"Petición creada por [@{github_user_name}]({github_user_url} \"Ver el perfil "
-"de @{github_user_name} en GitHub\") a través de la petición de GitHub "
-"[gh#{number} - {subject}]({github_url} \"Ir a 'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Petición creada a través de GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-"Commentario de [@{github_user_name}]({github_user_url} \"Ver el perfil de "
-"GitHub de @{github_user_name}\") a través de la petición de Github "
-"[gh#{number} - {subject}]({github_url} \"Ir a 'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentario a través de GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "El elemento referenciado no existe"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Estado cambiado desde un commit de GitLab"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "El estado no existe"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Creado desde Gitlab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentario de [@{gitlab_user_name}]({gitlab_user_url} \"Ver el perfil de "
-"@{gitlab_user_name}'s en GitLab\") desde GitLab.\n"
-"Petición de origen de GitLab: [gl#{number} - {subject}]({gitlab_url} \"Ir a "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentario desde GitLab:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Ver proyecto"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Ver sprints"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Ver historias de usuarios"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Ver tareas"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Ver peticiones"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Ver páginas del wiki"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Ver enlaces del wiki"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Solicitar afiliación"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Añadir historias de usuario al proyecto"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Agregar comentarios a historia de usuario"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Agregar comentarios a tareas"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Añadir peticiones"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Añadir comentarios a peticiones"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Agregar pagina wiki"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Modificar pagina wiki"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Agregar enlace wiki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modificar enlace wiki"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Añadir sprint"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modificar sprint"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Borrar sprint"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Ver historia de usuario"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Agregar historia de usuario"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Modificar historia de usuario"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Borrar historia de usuario"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Agregar tarea"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modificar tarea"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Borrar tarea"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Añadir petición"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Modificar petición"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Borrar petición"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Agregar pagina wiki"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Modificar pagina wiki"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Borrar pagina wiki"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Agregar enlace wiki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modificar enlace wiki"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Borrar enlace wiki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Modificar proyecto"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Agregar miembro"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Eliminar miembro"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Eliminar proyecto"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Agregar miembro"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Eliminar miembro"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Administrar valores de proyecto"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Administrar roles"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "Dueño"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Formato de imagen no válido"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Nombre de plantilla invalido"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Descripción de plantilla invalida"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr "id de usuario inválido"
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr "El usuario no existe"
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
-msgstr ""
+msgstr "El usuario debe ser un miembro del proyecto"
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
@@ -1396,158 +1359,233 @@ msgstr ""
"El proyecto debe tener un dueño y al menos uno de los usuarios debe ser un "
"administrador activo"
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "No tienes suficientes permisos para ver esto."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "La actualización parcial no está soportada."
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "Proyecto"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "típo de contenido"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "id de objeto"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "fecha modificada"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "archivo adjunto"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "está desactualizado"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "orden"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Personalizado"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
-msgstr ""
+msgstr "El proyecto esta bloqueado por un fallo en el pago"
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
-msgstr ""
+msgstr "El proyecto esta bloqueado por los administradores"
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
+msgstr "El proyecto esta bloqueado porque el dueño ha salido"
+
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Texto"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Texto multilínea"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Fecha"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr "Url"
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "tipo"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "valores"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "historia de usuario"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "tarea"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "petición"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Ya existe uno con el mismo nombre."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "estado"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "asunto"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "color"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "asignado a"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "requerido por el cliente"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "requerido por el equipo"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "El comentario ya ha sido borrado."
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "El comentario no se borro."
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Cambio"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Crear"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Borrar"
@@ -1603,7 +1641,7 @@ msgstr "borrado"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "No asignado"
@@ -1650,95 +1688,75 @@ msgstr "De:"
msgid "To:"
msgstr "A:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "contenido"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "nota de bloqueo"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "No tienes permisos para asignar un sprint a esta petición."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "No tienes permisos para asignar un estado a esta petición."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "No tienes permisos para establecer la gravedad de esta petición."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "No tienes permiso para establecer la prioridad de esta petición."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "No tienes permiso para establecer el tipo de esta petición."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "estado"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "gravedad"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "prioridad"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "sprint"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "fecha de finalización"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "asunto"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "asignado a"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "referencia externa"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Like"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Likes"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -1750,8 +1768,9 @@ msgstr "fecha estimada de comienzo"
msgid "estimated finish date"
msgstr "fecha estimada de finalización"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "está cerrada"
@@ -1765,291 +1784,385 @@ msgstr ""
"La fecha de inicio estimada debe ser previa a la fecha de finalización "
"estimada."
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "No hay sprints con este id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "está bloqueada"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "el parámetro '{param}' es obligatório"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "el parámetro 'project' es obligatório"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "creado el"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "texto extra de la invitación"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "orden del usuario"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "El usuario ya es miembro del proyecto"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "puntos por defecto"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "estado de historia por defecto"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "puntos por defecto"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "estado de tarea por defecto"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "prioridad por defecto"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "gravedad por defecto"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "estado de petición por defecto"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "tipo de petición por defecto"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "miembros"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total de sprints"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "puntos de historia totales"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "panel de backlog activado"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "panel de kanban activado"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "panel de wiki activo"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "panel de peticiones activo"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "sistema de videoconferencia"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "datos extra de videoconferencia"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "creación de plantilla"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "permisos de anónimo"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "permisos de usuario"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "privado"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "permisos de anónimo"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "permisos de usuario"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr "es destacado"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr "está buscando a gente"
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr "nota (buscando a gente)"
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "colores de etiquetas"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr "token de transferencia de proyecto"
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr "código bloqueado"
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "fecha y hora de actualización"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "recuento"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr "fans la última semana"
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr "fans el último mes"
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr "fans el último año"
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr "actividad la última semana"
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr "actividad el último mes"
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr "actividad el último áño"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "configuración de modulos"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "archivado"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "color"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "limite del trabajo en progreso"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "rol por defecto para el propietario"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "opciones por defecto"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "estatuas de historias"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "puntos"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "estatus de tareas"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "estados de petición"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "tipos de petición"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "prioridades"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "gravedades"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "roles"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Involucrado"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Todas"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Ninguna"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "fecha y hora de creación"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "entradas del histórico"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "usuarios notificados"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Observado"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr ""
"Ya existe una política de notificación para este usuario en el proyecto."
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Valor inválido para el nivel de notificación"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2757,161 +2870,183 @@ msgstr ""
"\n"
"[%(project)s] Borrada la página del wiki \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Los observadores tienen usuarios invalidos"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "La versión debe ser un número entero"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "La versión no es válida"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Las version difiere de la actual"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "versión"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
"No puedes abandonar el proyecto si eres el dueño o no existen más "
"administradores"
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "La dirección de email ya está en uso."
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Rol inválido para el proyecto"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Opciones por defecto"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Estados de historia de usuario"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Puntos"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Estado de tareas"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Estados de peticion"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Tipos de petición"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Prioridades"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Gravedades"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Roles"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
+msgstr "Ha alcanzado el limite de miembros para proyectos privados"
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
+msgstr "Ha alcanzado el limite de miembros para proyectos públicos"
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr "No puedes tener más proyectos privados"
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
+"Este proyecto alcanzo el limite actual de miembros para proyectos privados"
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr "No puedes tener más proyectos públicos"
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
+"Este proyecto alcanzo su limite actual de miembros para proyectos publicos"
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Sprint futuro"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Final de proyecto"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "token inválido"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr "El token ha expirado"
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "etiquetas"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "colores de etiquetas"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "No tienes permisos para asignar este sprint a esta tarea."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr "No tienes permisos para asignar esta historia a esta tarea."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "No tienes permisos para asignar este estado a esta tarea."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "orden en la historia"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "orden en el taskboard"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "tiene iocaína"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "No existe ninguna tarea con este id"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3068,11 +3203,16 @@ msgid ""
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
+"\n"
+" Hola %(old_owner_name)s,
\n"
+" %(new_owner_name)s acepto su oferta y sera el nuevo dueño del "
+"proyecto \"%(project_name)s\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
#, python-format
msgid "%(new_owner_name)s says:
"
-msgstr ""
+msgstr "%(new_owner_name)s dice:
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
@@ -3081,6 +3221,9 @@ msgid ""
"p>\n"
" "
msgstr ""
+"\n"
+" Desde ahora su nuevo estado para este proyecto es \"admin\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
#, python-format
@@ -3098,7 +3241,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
-msgstr ""
+msgstr "%(new_owner_name)s dice:"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
@@ -3137,6 +3280,11 @@ msgid ""
"new project owner for \"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+" Hola %(owner_name)s,
\n"
+" %(rejecter_name)s declino su oferta y no sera el nuevo dueño del "
+"proyecto \"%(project_name)s\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
#, python-format
@@ -3145,6 +3293,9 @@ msgid ""
" %(rejecter_name)s says:
\n"
" "
msgstr ""
+"\n"
+" %(rejecter_name)s dice:
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
@@ -3153,6 +3304,10 @@ msgid ""
"different person.\n"
" "
msgstr ""
+"\n"
+" Si lo desea, aun puede transferir el dominio del proyecto a otra "
+"persona.
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
@@ -3175,7 +3330,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
msgid "%(rejecter_name)s says:"
-msgstr ""
+msgstr "%(rejecter_name)s dice:"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
@@ -3210,6 +3365,11 @@ msgid ""
"\"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+" Hola %(owner_name)s,
\n"
+" %(requester_name)s ha solicitado convertirse en el dueño del "
+"proyecto \"%(project_name)s\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
@@ -3218,6 +3378,10 @@ msgid ""
"project transfer from the administration panel.\n"
" "
msgstr ""
+"\n"
+" Por favor, Haga click en \"Continuar\" Si desea iniciar la "
+"transferencia del proyecto desde el panel de administracion.
\n"
+" "
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
@@ -3232,6 +3396,10 @@ msgid ""
"%(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Hola %(owner_name)s,\n"
+"%(requester_name)s ha solicitado ser el dueño del proyecto \"%(project_name)s"
+"\".\n"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
msgid ""
@@ -3239,10 +3407,13 @@ msgid ""
"Please, go to your project settings if you would like to start the project "
"transfer from the administration panel.\n"
msgstr ""
+"\n"
+"Por favor, vaya a la configuracion del proyecto si desea iniciar la "
+"tranferencia del proyecto desde el panel de administracion.\n"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
msgid "Go to your project settings:"
-msgstr ""
+msgstr "Ir a la configuracion del proyecto:"
#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
#, python-format
@@ -3250,6 +3421,8 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer request\n"
msgstr ""
+"\n"
+"[%(project)s] Solicitud de transferencia de dominio del proyecto\n"
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
#, python-format
@@ -3260,6 +3433,11 @@ msgid ""
"would like you to become the new project owner.\n"
" "
msgstr ""
+"\n"
+" Hola %(receiver_name)s,
\n"
+" %(owner_name)s, el dueño del proyecto \"%(project_name)s\" desea "
+"que usted sea el nuevo dueño del proyecto.
\n"
+" "
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
#, python-format
@@ -3268,6 +3446,9 @@ msgid ""
" %(owner_name)s says:
\n"
" "
msgstr ""
+"\n"
+" %(owner_name)s dice:
\n"
+" "
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
@@ -3276,6 +3457,10 @@ msgid ""
"proposal.\n"
" "
msgstr ""
+"\n"
+" Por favor, Haga click en \"Continuar\" Para aceptar o rechazar "
+"esta propuesta.
\n"
+" "
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
#, python-format
@@ -3285,11 +3470,15 @@ msgid ""
"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
"you to become the new project owner.\n"
msgstr ""
+"\n"
+"Hola %(receiver_name)s,\n"
+"%(owner_name)s, el dueño del proyecto \"%(project_name)s\" desea que usted "
+"sea el nuevo dueño del proyecto\n"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
#, python-format
msgid "%(owner_name)s says:"
-msgstr ""
+msgstr "%(owner_name)s dice:"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
@@ -3297,10 +3486,13 @@ msgid ""
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
+"\n"
+"Por favor, Vaya al siguiente link para aceptar o rechazar esta propuesta."
+"p>\n"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project ownership transfer:"
-msgstr ""
+msgstr "Aceptar o rechazar la transferencia del dominio del proyecto:"
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
#, python-format
@@ -3308,14 +3500,16 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer offer\n"
msgstr ""
+"\n"
+"[%(project)s] Oferta de transferencia de dominio del proyecto\n"
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3333,12 +3527,12 @@ msgstr ""
"las nuevas necesidades del cliente."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3354,315 +3548,401 @@ msgstr ""
"diferentes colas."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Nueva"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Preparada"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "En curso"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Lista para testear"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Hecha"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Archivada"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Cerrada"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Necesita información"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Pospuesta"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Rechazada"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Bug"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Pregunta"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Mejora"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Baja"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Alta"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Deseada"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Menor"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Importante"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Crítica"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Diseñador"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Product Owner"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"No tienes permisos para asignar este sprint a esta historia de usuario."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"No tienes permisos para asignar este estado a esta historia de usuario."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Generada la historia de usuario #{ref} - {subject}"
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rol"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "orden en el backlog"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "orden en el sprint"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "fecha de finalización"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "requerido por el cliente"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "requerido por el equipo"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "generada desde una petición"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "No existe ninguna historia de usuario con este id"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "No existe ningún proyecto con este id"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "No existe ningún estado de historia con este id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "La dirección de email ya está en uso."
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "No existe ningún estado de tarea con este id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Rol inválido para el proyecto"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr "El dueño del proyecto debe ser administrador"
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+"Por lo menos un usuario debe ser administrador activo para este proyecto"
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Opciones por defecto"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Estados de historia de usuario"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Puntos"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Estado de tareas"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Estados de peticion"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Tipos de petición"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioridades"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Gravedades"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Roles"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Votos"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Voto"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "el parámetro 'content' es obligatório"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "el parámetro 'project_id' es obligatório"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "última modificación por"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Comprueba la API de histórico para obtener el diff exacto"
-#: taiga/users/admin.py:38
-msgid "Project Member"
-msgstr ""
-
#: taiga/users/admin.py:39
-msgid "Project Members"
-msgstr ""
+msgid "Project Member"
+msgstr "Miembro del proyecto"
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:40
+msgid "Project Members"
+msgstr "Miembros del proyecto"
+
+#: taiga/users/admin.py:50
msgid "id"
-msgstr ""
+msgstr "Id"
#: taiga/users/admin.py:81
msgid "Project Ownership"
-msgstr ""
+msgstr "Dueño del proyecto"
#: taiga/users/admin.py:82
msgid "Project Ownerships"
-msgstr ""
+msgstr "Dueños del proyecto"
#: taiga/users/admin.py:119
msgid "Personal info"
@@ -3680,53 +3960,53 @@ msgstr "Restricciones"
msgid "Important dates"
msgstr "datos importántes"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Email duplicado"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Email no válido"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Nombre de usuario o email no válidos"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "¡Correo enviado con éxito!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "La contraseña actual es obligatoria."
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "La nueva contraseña es obligatoria"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "La longitud de la contraseña debe de ser de al menos 6 caracteres"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Contraseña actual inválida"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Invalido, ¿estás seguro de que el token es correcto y no se ha usado antes?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Inválido, ¿estás seguro de que el token es correcto?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "es superusuario"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3734,24 +4014,24 @@ msgstr ""
"Otorga todos los permisos a este usuario sin necesidad de hacerlo "
"explicitamente."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "nombre de usuario"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Obligatorio. 30 caracteres o menos. Letras, números y /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Introduce un nombre de usuario válido"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "activo"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3759,71 +4039,63 @@ msgstr ""
"Denota a los usuarios activos. Desmárcalo para dar de baja/borrar a un "
"usuario."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografía"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "fecha de registro"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "idioma por defecto"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "tema por defecto"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "zona horaria por defecto"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "añade color a las etiquetas"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "token de email"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "nueva dirección de email"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
-msgstr ""
+msgstr "numero maximo de proyectos privados asignados"
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
-msgstr ""
+msgstr "numero maximo de proyectos publicos asignados"
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr "máximo de membresías para cada proyecto privado poseído"
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr "máximo de membresías para cada proyecto público poseído"
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "permisos"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "no válido"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Nombre de usuario inválido. Prueba con otro."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Nombre de usuario o contraseña inválidos."
@@ -4014,47 +4286,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "¡Te hemos Taigaizado!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "No existe ningún rol con este id"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "no válido"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Nombre de usuario inválido. Prueba con otro."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr "Violación de una restricción de unicidad. La clave '{}' ya existe."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "clave"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "clave secreta"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "código de estado"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "datos de petición"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "cabeceras de la petición"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "datos de respuesta"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "cabeceras de la respuesta"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duración"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 4034a724..5139f702 100644
--- a/taiga/locale/fi/LC_MESSAGES/django.po
+++ b/taiga/locale/fi/LC_MESSAGES/django.po
@@ -5,12 +5,13 @@
# Translators:
# artol , 2015
# David Barragán , 2015
+# Sami Singh , 2016
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -20,164 +21,168 @@ msgstr ""
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Julkinen rekisteri on suljettu."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "väärä rekisterin tyyppi"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "väärä kirjautumistyyppi"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
-msgid "invalid username"
-msgstr "tuntematon käyttäjänimi"
-
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
-msgid ""
-"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
-msgstr ""
-"Vaaditaan. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'"
-
-#: taiga/auth/services.py:75
+#: taiga/auth/services.py:76
msgid "Username is already in use."
msgstr "Käyttäjänimi on varattu."
-#: taiga/auth/services.py:78
+#: taiga/auth/services.py:79
msgid "Email is already in use."
msgstr "Sähköposti on jo varattu."
-#: taiga/auth/services.py:94
+#: taiga/auth/services.py:95
msgid "Token not matches any valid invitation."
msgstr "Tunniste ei vastaa mihinkään avoimeen kutsuun."
-#: taiga/auth/services.py:122
+#: taiga/auth/services.py:123
msgid "User is already registered."
msgstr "Käyttäjä on jo rekisteröitynyt."
-#: taiga/auth/services.py:146
+#: taiga/auth/services.py:147
msgid "This user is already a member of the project."
-msgstr ""
+msgstr "Tämä käyttäjä on jo projektin jäsen."
-#: taiga/auth/services.py:172
+#: taiga/auth/services.py:173
msgid "Error on creating new user."
msgstr "Virhe käyttäjän luonnissa."
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
msgid "Invalid token"
msgstr "Väärä tunniste"
-#: taiga/base/api/fields.py:292
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
+msgid "invalid username"
+msgstr "tuntematon käyttäjänimi"
+
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
+msgid ""
+"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
+msgstr ""
+"Pakollinen. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'"
+
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Pakollinen kenttä."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Virheellinen arvo."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' pitää olla True tai False."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Anna kelvollinen 'avain' joka koostuu merkeistä, numeroista, alaviivoista ja "
"tavuviivoista."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "Valitse kelvollinen valinta. %(value)s ei ole yksi vaihtoehdoista."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Anna voimassaoleva sähköpostiosoite."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "Päivämäärä on väärässä muodossa. Käytä yhtä näistä muodoista: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "Päiväys on väärässä muodossa. Käytä yhtä näistä muodoista: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "Aika on väärässä muodossa. Käytä yhtä näistä muodoista: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Anna kokonaisluku."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Varmista että arvo on korkeintaan %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Varmista että arvo on vähintään %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" pitää olla desimaaliluku."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Anna numero."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Anna korkeintaan %s numeroa yhteensä."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Desimaaleja voi olla korkeintaan %s."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Ennen desimaalipistettä saa olla korkeintaan %s numeroa."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Tiedostoa ei lähtetty. Varmista merkistö lomakkeella."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Tiedostoa ei lähetetty."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Tiedosto oli tyhjä."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
msgstr ""
"Tiedoston nimi saa olla korkeintaan %(max)d pitkä se on nyt %(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "Valitse tiedosto tai Poista valintaneliö, ei molempia."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -185,180 +190,177 @@ msgstr ""
"Anna kelvollinen kuva. Annettu ei ollut tunnistettava kuva tai se oli "
"vioittunut."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
-msgstr ""
+msgstr "Estetty elementti"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Sivu ei ole 'viimeinen', ekä sitä pystytä muuntamaan numeroksi."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Virheellinen sivu (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Virheellinen oikeuksien määrittely."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Virheellinen pk '%s' - sitä ei löydy."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Väärä tyyppi. Odotetaan pk-arvoa, vastaanotettu %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Kohdetta jossa %s=%s ei ole."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Viallinen linkki - URL ei löydy"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Viallinen linkki - URL ei löydy"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Virheellinen linkki konfiguraatiovirheen takia"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Virheellinen linkki - kohdetta ei löydy."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Väärä tyyppi. Odotan URL-merkkijonoa, sain %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Virheellinen data"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Syöte puuttuu"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr "En voi luoda uutta kohdetta, vain olemassaolevat voidaan päivittää."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Anna lista kohteista."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Ei löytynyt"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Ei käyttöoikeutta"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Palvelinsovelluksen virhe"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Yhteysvirhe."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Virheellinen pyyntö."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Virheelliset tunnistautumistiedot."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Kirjautumistiedot puuttuvat."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Sinulla ei ole tähän oikeuksia."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Method '%s' not allowed."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Could not satisfy the request's Accept header"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Unsupported media type '%s' in request."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Request was throttled."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Tulee saataville %d sekunttia %s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Odottamaton virhe"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Ei löytynyt."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Method not supported for this endpoint."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Väärät argumentit."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Data validation error"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Integrity Error for wrong or invalid arguments"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Precondition error"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
-msgstr ""
+msgstr "Ei enää tilaa uusille projekteille."
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Error in filter params types."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' must be an integer value."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "avainsanat"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -413,7 +415,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -425,26 +427,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" Taiga tuki:"
-"strong>\n"
-" %(support_url)s\n"
-"
\n"
-" Ota yhteyttä:"
-"strong>\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Postituslista:"
-"strong>\n"
-" \n"
-" %(mailing_list_url)s\n"
-" \n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -482,6 +464,9 @@ msgid ""
"%(comment)s\n"
" "
msgstr ""
+"\n"
+"kommentti:
\n"
+"%(comment)s
"
#: taiga/base/templates/emails/updates-body-text.jinja:6
#, python-format
@@ -493,103 +478,88 @@ msgstr ""
"\n"
"Kommentti: %(comment)s"
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
-msgstr ""
+msgstr "Tarvitsemme ainakin yhden roolin"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Tarvitaan tiedosto"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Virheellinen tiedostomuoto"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" ei löytynyt tästä projektista"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Virheellinen sisältä, pitää olla muodossa {\"avain\": \"arvo\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Sisältää vieheellisiä omia kenttiä."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Nimi on tuplana projektille"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "virhe projektidatan tuonnissa"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "virhe roolien tuonnissa"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "virhe jäsenyyksien tuonnissa"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "virhe atribuuttilistan tuonnissa"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "virhe oletusarvojen tuonnissa"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "virhe omien arvojen tuonnissa"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "virhe kierroksien tuonnissa"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "virhe käyttäjätarinoiden tuonnissa"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "virhe tehtävien tuonnissa"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "virhe pyyntöjen tuonnissa"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "virhe käyttäjätarinoiden tuonnissa"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "virhe tehtävien tuonnissa"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "virhe wiki-sivujen tuonnissa"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "virhe viki-linkkien tuonnissa"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "virhe avainsanojen sisäänlukemisessa"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "virhe aikajanojen tuonnissa"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
-msgstr ""
+msgstr "odottamaton virhe projektia tuotaessa"
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Virhe tiedoston luonnissa"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -609,15 +579,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Virhe tiedoston latauksessa"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -853,77 +823,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Projetkisi tiedosto on luettu sisään"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" ei löytynyt tästä projektista"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Virheellinen sisältä, pitää olla muodossa {\"avain\": \"arvo\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Sisältää vieheellisiä omia kenttiä."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Nimi on tuplana projektille"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr ""
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "nimi"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr ""
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "kuvaus"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr ""
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr ""
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr ""
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "koko nimi"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "sähköpostiosoite"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "kommentti"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "luontipvm"
@@ -955,7 +945,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Lisätiedot"
@@ -989,522 +979,577 @@ msgstr ""
"\n"
"[Taiga] Palautetta käyttäjältä %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "The payload is not a valid json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Projektia ei löydy"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Virheellinen allekirjoitus"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Viitattu elementtiä ei löydy"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Tilaa ei löydy"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Tila muutettu BitBucket kommitilla"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Virheellinen pyynnön tieto"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Virheellinen pyynnön kommentin tieto"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Virheellinen pyynnön tieto"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Tila muutettu GitHub commitin toimesta."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"Pyyntö luotu [@{github_user_name}]({github_user_url} \"Katso "
-"@{github_user_name}'s GitHub profile\") GitHubista.\n"
-"ALkuperäinen GitHub pyyntö: [gh#{number} - {subject}]({github_url} \"Siirry "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Pyyntö luotu GitHubista"
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Kommentti [@{github_user_name}]({github_user_url} \"Katso "
-"@{github_user_name}'s GitHub profile\") GitHubista.\n"
-"Alkuperäinen GitHub pyyntö: [gh#{number} - {subject}]({github_url} \"Siirry "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Kommentti GitHubista:\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Tila muutettu GitLab kommitilla"
-
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Luotu GitLabissa"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Viitattu elementtiä ei löydy"
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Tilaa ei löydy"
+
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Katso projektia"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Katso virstapylvästä"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Katso käyttäjätarinoita"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Katso tehtäviä"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Katso pyyntöjä"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Katso wiki-sivuja"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Katso wiki-linkkejä"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Pyydä jäsenyyttä"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Lisää käyttäjätarina projektiin"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Lisää kommentteja käyttäjätarinoihin"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Lisää kommentteja tehtäviin"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Lisää pyyntöjä"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Lisää kommentteja pyyntöihin"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Lisää wiki-sivu"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Muokkaa wiki-sivua"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Lisää wiki-linkki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Muokkaa wiki-linkkiä"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Lisää virstapylväs"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Muokkaa virstapyvästä"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Poista virstapylväs"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Katso käyttäjätarinaa"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Lisää käyttäjätarina"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Muokkaa käyttäjätarinaa"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Poista käyttäjätarina"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Lisää tehtävä"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Muokkaa tehtävää"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Poista tehtävä"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Lisää pyyntö"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Muokkaa pyyntöä"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Poista pyyntö"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Lisää wiki-sivu"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Muokkaa wiki-sivua"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Poista wiki-sivu"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Lisää wiki-linkki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Muokkaa wiki-linkkiä"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Poista wiki-linkki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Muokkaa projekti"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Lisää jäsen"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Poista jäsen"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Poista projekti"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Lisää jäsen"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Poista jäsen"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Hallinnoi projektin arvoja"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Hallinnoi rooleja"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "omistaja"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Puutteelliset argumentit"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Väärä kuvaformaatti"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Virheellinen mallipohjan nimi"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Virheellinen mallipohjan kuvaus"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Sinulla ei ole oikeuksia nähdä tätä."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "Projekti ID ei vastaa kohdetta ja projektia"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "projekti"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "sisältötyyppi"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "objekti ID"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "muokkauspvm"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "liite"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "on poistettu"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "order"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr ""
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
-msgid "Text"
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:29
-msgid "Date"
+msgid "Multi-Line Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:30
+msgid "Date"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "tyyppi"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "arvot"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "käyttäjätarina"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "tehtävä"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "pyyntö"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Nimi on jo olemassa"
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "viittaus"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "tila"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "aihe"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "väri"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "tekijä"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "on asiakkaan vaatimus"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "on tiimin vaatimus"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Kommentti on jo poistettu"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Kommenttia ei poistettu"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Muokkaa"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Luo"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Poista"
@@ -1560,7 +1605,7 @@ msgstr "poistettu"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Tekijä puuttuu"
@@ -1607,95 +1652,75 @@ msgstr "Keneltä:"
msgid "To:"
msgstr "Kenelle:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "sisältö"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "suljettu muistiinpano"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Sinulla ei ole oikeuksia laittaa kierrosta tälle pyynnölle."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Sinulla ei ole oikeutta asettaa statusta tälle pyyntö."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "Sinulla ei ole oikeutta asettaa vakavuutta tälle pyynnölle."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "Sinulla ei ole oikeutta asettaa kiireellisyyttä tälle pyynnölle."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Sinulla ei ole oikeutta asettaa tyyppiä tälle pyyntö."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "viittaus"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "tila"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "vakavuus"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "kiireellisyys"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "virstapylväs"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "loppupvm"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "aihe"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "tekijä"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "ulkoinen viittaus"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr ""
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "hukka-aika"
@@ -1707,8 +1732,9 @@ msgstr "arvioitu alkupvm"
msgid "estimated finish date"
msgstr "arvioitu loppupvm"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "on suljettu"
@@ -1720,290 +1746,384 @@ msgstr "disponibility"
msgid "The estimated start must be previous to the estimated finish."
msgstr "Alkuajan pitää olla ennen loppuaikaa."
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Kierrosta tällä ID:llä ei ole"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "on lukittu"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' parametri on pakollinen"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project' parametri on pakollinen"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "sähköposti"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "luo täällä"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "tunniste"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "kutsun lisäteksti"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "käyttäjäjärjestys"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "Käyttäjä on jo projektin jäsen"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "oletuspisteet"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "oletus Kt tila"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "oletuspisteet"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "oletus tehtävän tila"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "oletus kiireellisyys"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "oletus vakavuus"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "oletus pyynnön tila"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "oletus pyyntö tyyppi"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "jäsenet"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "virstapyväitä yhteensä"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "käyttäjätarinan yhteispisteet"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "aktiivinen odottavien paneeli"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "aktiivinen kanban-paneeli"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "aktiivinen wiki-paneeli"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "aktiivinen pyyntöpaneeli"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "videokokous järjestelmä"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "luo mallipohja"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "vieraan oikeudet"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "käyttäjän oikeudet"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "on yksityinen"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "vieraan oikeudet"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "käyttäjän oikeudet"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "avainsanojen värit"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "päivityspvm"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "moduulien asetukset"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "on arkistoitu"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "väri"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "työn alla olevien max"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "arvo"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "oletus omistajan rooli"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "oletus optiot"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "kt tilat"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "pisteet"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "tehtävän tilat"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "pyyntöjen tilat"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "pyyntötyypit"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "kiireellisyydet"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "vakavuudet"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "roolit"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr ""
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr ""
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "luontipvm"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "historian kohteet"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "ilmoita käyttäjille"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr ""
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Ilmoita olemassaolosta määritellyille käyttäjille ja projektille"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr ""
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2721,159 +2841,179 @@ msgstr ""
"\n"
"[%(project)s] Poistettiin wiki-sivu \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Vahdit sisältävät virheellisiä käyttäjiä"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "Versio pitää olla kokonaisluku"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr ""
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Versio ei ole sama kuin nykyinen"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "versio"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Sähköpostiosoite on jo käytössä"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Virheellinen rooli projektille"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Oletusoptiot"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Käyttäjätarinatilat"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Pisteet"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Tehtävien tilat"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Pyyntöjen tilat"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "pyyntötyypit"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Kiireellisyydet"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Vakavuudet"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Roolit"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Tuleva kierros"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Projektin loppu"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Tunniste on virheellinen"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "avainsanat"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "avainsanojen värit"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr ""
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "kt järjestys"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "tehtävätaulun järjestys"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "on hidaste"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "En löydä tehtävää tällä id:llä."
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3254,12 +3394,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3275,12 +3415,12 @@ msgstr ""
"kasvaa ja muuttua kun tuotteesta ja asiakkaista opitaan lisää."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3293,303 +3433,388 @@ msgstr ""
"jäsenille."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Uusi"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Valmis"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "Työn alla"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Valmis testattavaksi"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Tehty"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Arkistoitu"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Suljettu"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Tarvitsee lisätietoja"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Siirretty odottamaan"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Hylätty"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Virhe"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Kysymys"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Uusi ominaisuus"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Matala"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normaali"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Korkea"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Toivelista"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Vähäpätöinen"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Tärkeä"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Kriittinen"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "Käyttäjäkokemus"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Suunnittelu"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Edusta"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Palvelin"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Tuoteomistaja"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Sidosryhmä"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rooli"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "odottavien listan järjestys"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "kierros järjestys"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "loppupvm"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "on asiakkaan vaatimus"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "on tiimin vaatimus"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "luotu pyynnöstä"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "En löydä käyttäjätarinaa tällä id:llä"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "En löydä projektia tällä id:llä"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "En löydä käyttäjätarinan tilaa tällä id:llä"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Sähköpostiosoite on jo käytössä"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "En löydä tehtävän tilaa tällä id:llä"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Virheellinen rooli projektille"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Oletusoptiot"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Käyttäjätarinatilat"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Pisteet"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Tehtävien tilat"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Pyyntöjen tilat"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "pyyntötyypit"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Kiireellisyydet"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Vakavuudet"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Roolit"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Ääniä"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Äänestä"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'content' parametri on pakollinen"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parametri on pakollinen"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "viimeksi muokannut"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3617,151 +3842,143 @@ msgstr ""
msgid "Important dates"
msgstr "Tärkeät päivämäärät"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Sähköposti on jo olemassa"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Virheellinen sähköposti"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Tuntematon käyttäjänimi tai sähköposti"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Sähköposti lähetetty."
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Nykyinen salasanaparametri tarvitaan"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Uusi salasanaparametri tarvitaan"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Salasanan pitää olla vähintään 6 merkkiä pitkä"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Virheellinen nykyinen salasana"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Virheellinen. Oletko varma, että tunniste on oikea ja et ole jo käyttänyt "
"sitä?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Virheellinen, oletko varma että tunniste on oikea?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "pääkäyttäjän status"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
"Kertoo että käyttäjä saa tehdä kaiken ilman erikseen annettuja oiekuksia."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "käyttäjänimi"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Vaaditaan. Korkeintaan 30merkkiä. Kirjaimet, numerot ja merkit /./-/_ "
"sallittuja"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Anna olemassa oleva käyttäjänimi."
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "aktiivinen"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
"Käyttäjä on aktiivinen. Poista aktiivisuus käyttäjän poistamisen sijaan."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "kuva"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "liittymispvm"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "oletuskieli"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "oletus aikavyöhyke"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "väritä avainsanat"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "sähköpostitunniste"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "uusi sähköpostiosoite"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "oikeudet"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "virheellinen"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Tuntematon käyttäjänimi, yritä uudelleen."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Käyttäjätunnus tai salasana eivät ole oikein."
@@ -3943,48 +4160,52 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Olet nyt Taigatettu!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "En löydä roolia tällä id:llä"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "virheellinen"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Tuntematon käyttäjänimi, yritä uudelleen."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "key"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "secret key"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "status code"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "request data"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "request headers"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "response data"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "response headers"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duration"
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index 61fa72db..f579991b 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -23,8 +23,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@@ -34,161 +34,165 @@ msgstr ""
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "L'inscription publique est désactivée."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "type d'inscription invalide"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "type d'identifiant invalide"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Ce nom d'utilisateur est déjà utilisé."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Cette adresse email est déjà utilisée."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Le jeton ne correspond à aucune invitation."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Cet utilisateur est déjà inscrit."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "L'utilisateur est déjà un membre du projet"
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Erreur à la création du nouvel utilisateur."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Jeton invalide"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "nom d'utilisateur invalide"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
"Requis. 255 caractères ou moins. Lettres, chiffres et caractères /./-/_'"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Ce nom d'utilisateur est déjà utilisé."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Cette adresse email est déjà utilisée."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Le jeton ne correspond à aucune invitation."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Cet utilisateur est déjà inscrit."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "L'utilisateur est déjà un membre du projet"
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Erreur à la création du nouvel utilisateur."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Jeton invalide"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Ce champ est requis."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Valeur invalide."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "La valeur de '%s' doit être soit Vrai soit Faux."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Entrez un 'slug' valide composé de lettres, chiffres, tirets bas ou traits "
"d'union."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Sélectionnez une option valide. %(value)s ne fait pas partie des choix "
"possibles."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Entrez une adresse email valide."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr ""
"Le format de la date est mauvais. Utilisez un de ces formats à la place: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr ""
"Le format de l'horodatage est mauvais. Utilisez un de ces formats à la "
"place: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr ""
"Le format de l'heure est mauvais. Utilisez un de ces formats à la place: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Entrez un nombre entier."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr ""
"Assurez-vous que cette valeur est inférieure ou égale à %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr ""
"Assurez-vous que cette valeur est supérieure ou égale à %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "La valeur de \"%s\" doit être un nombre en virgule flottante."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Entrez un nombre."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres au total."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Assurez-vous qu'il n'y a pas plus de %s décimales."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres avant le point."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Aucun fichier n'a été soumis. Vérifiez l'encodage sur le formulaire. "
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Aucun fichier n'a été soumis."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Le fichier soumis est vide."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -196,13 +200,13 @@ msgstr ""
"Assurez-vous que le nom de fichier comporte au plus %(max)d caractères (il "
"en a %(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Veuillez soit soumettre un fichier ou cocher la case de remise à zéro, mais "
"pas les deux."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -210,184 +214,181 @@ msgstr ""
"Envoyez une image valide. Le fichier que vous avez envoyé n'était pas une "
"image ou était une image corrompue."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Élément bloqué"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
"La page n'est pas la \"dernière\", et ne peut pas non plus être convertie en "
"entier."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Page invalide (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Définition de permission invalide."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Pk '%s' invalide - l'objet n'existe pas."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Type incorrect. Valeur pk attendue, %s reçu."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "L'objet pour lequel %s=%s n'existe pas."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Hyperlien invalide - aucune correspondance d'URL."
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Hyperlien invalide - Correspondance d'URL incorrecte."
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Hyperlien invalide dû à une erreur de configuration"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Hyperlien invalide - l'objet n'existe pas."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Type incorrect. Chaîne URL attendu, %s reçu."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Donnée invalide"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Aucune entrée fournie"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Impossible de créer un nouvel élément, seuls les éléments existants peuvent "
"être mis à jour."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Une liste d'éléments était attendue."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Non trouvé"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Permission refusée"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Erreur du serveur d'application"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Erreur de connexion."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Requête mal formée."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Informations de connexion incorrects."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Informations d'authentification manquantes."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Vous n'avez pas l'autorisation d'effectuer cette action."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "La méthode %s n'est pas autorisée"
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Impossible de satisfaire l'en-tête Accept"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Type de média %s non pris en charge dans la requête."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "La requête a été limitée"
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Disponible dans %d seconde%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Erreur inattendue"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Non trouvé."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Méthode non supportée par ce point d'entrée"
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Arguments invalides."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Erreur de validation des données"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Erreur d'intégrité ou arguments invalides"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Erreur de précondition"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr "Limite de projets atteinte."
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Erreur dans les types de paramètres de filtres"
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' doit être une valeur entière."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "tags"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -442,7 +443,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -454,26 +455,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" Support de "
-"Taiga :\n"
-" %(support_url)s\n"
-"
\n"
-" Nous contacter :"
-"\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Groupe de "
-"discussion :\n"
-" \n"
-" %(mailing_list_url)s\n"
-" \n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -528,104 +509,89 @@ msgstr ""
" Commentaire : %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Veuillez sélectionner au moins un rôle."
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Fichier de dump obligatoire"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Format de dump invalide"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" non trouvé dans the projet"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Format non valide. Il doit être de la forme {\"cle\": \"valeur\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Contient des champs personnalisés non valides."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Nom dupliqué pour ce projet"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "Erreur lors de l'importation de données"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "Erreur à l'importation des rôles"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "Erreur à l'importation des groupes d'utilisateurs"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "erreur lors de l'importation des listes des attributs de projet"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr ""
"erreur lors de l'importation des valeurs par défaut des attributs de projet"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "Erreur à l'importation des champs personnalisés"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "Erreur lors de l'importation des sprints."
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "erreur à l'importation des histoires utilisateur"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "Erreur lors de l'importation des tâches."
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "erreur à l'importation des problèmes"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "erreur à l'importation des histoires utilisateur"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "Erreur lors de l'importation des tâches."
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "Erreur à l'importation des pages Wiki"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "Erreur à l'importation des liens Wiki"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "erreur lors de l'importation des mots-clés"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "erreur lors de l'import des timelines"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Erreur dans la génération du dump du projet"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -645,15 +611,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Erreur au chargement du dump du projet"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -889,77 +855,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Votre projet à été importé"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" non trouvé dans the projet"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Format non valide. Il doit être de la forme {\"cle\": \"valeur\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Contient des champs personnalisés non valides."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Nom dupliqué pour ce projet"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Authentification requise"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "nom"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "Url de l'icône"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "description"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Url suivante"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "Clé secrète pour chiffrer le jeton de l'application"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "utilisateur"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "application"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "Nom complet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "Adresse email"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "Commentaire"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "Date de création"
@@ -989,7 +975,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informations supplémentaires"
@@ -1023,356 +1009,345 @@ msgstr ""
"\n"
"[Taiga] Réaction de %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "Le payload n'est pas un json valide"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Le projet n'existe pas"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Signature non valide"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "L'élément référencé n'existe pas"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "L'état n'existe pas"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Statut changé depuis un commit BitBucket"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Information incorrecte sur le problème"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Ticket créé depuis BitBucket."
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Ignoré"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Information incorrecte sur le problème"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Commentaire depuis BitBucket :\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Statut changé depuis un commit GitHub."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Suivi de problème créé à partir de GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Commentaire provenant de GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "L'élément référencé n'existe pas"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Statut changé depuis un commit GitLab"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "L'état n'existe pas"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Créé à partir de GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Commentaire depuis GitLab:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Consulter le projet"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Voir les jalons"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Voir les histoires utilisateur"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Consulter les tâches"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Voir les problèmes"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Consulter les pages Wiki"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Consulter les liens Wiki"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Demander à devenir membre"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Ajouter l'histoire utilisateur au projet"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Ajouter des commentaires aux histoires utilisateur"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Ajouter des commentaires à une tâche"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Ajouter des problèmes"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Ajouter des commentaires aux problèmes"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Ajouter une page Wiki"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Modifier une page Wiki"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Ajouter un lien Wiki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modifier un lien Wiki"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Ajouter un jalon"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modifier le jalon"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Supprimer le jalon"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Voir l'histoire utilisateur"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Ajouter une histoire utilisateur"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Modifier l'histoire utilisateur"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Supprimer l'histoire utilisateur"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Ajouter une tâche"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modifier une tâche"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Supprimer une tâche"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Ajouter un problème"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Modifier le problème"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Supprimer le problème"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Ajouter une page Wiki"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Modifier une page Wiki"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Supprimer une page Wiki"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Ajouter un lien Wiki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modifier un lien Wiki"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Supprimer un lien Wiki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Modifier le projet"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Ajouter un membre"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Supprimer un membre"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Supprimer le projet"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Ajouter un membre"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Supprimer un membre"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Administrer les paramètres du projet"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Administrer les rôles"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "propriétaire"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "arguments manquants"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "format de l'image non valide"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Nom de modèle non valide"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Description du modèle non valide"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr "Identifiant utilisateur invalide"
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr "L'utilisateur n'existe pas"
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr "L'utilisateur doit déjà être un membre du projet"
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
@@ -1380,158 +1355,233 @@ msgstr ""
"Le projet doit avoir un propriétaire et au moins l'un de ses membres doit "
"être un administrateur actif."
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Vous n'avez pas les permissions pour consulter cet élément"
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Mises à jour partielles non supportées"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "projet"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "type du contenu"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "identifiant de l'objet"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "état modifié"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "pièces jointes"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "est obsolète"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "ordre"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Personnalisé"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr "Ce projet a été bloqué pour cause d'impayé"
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr "Ce projet a été bloqué par l'équipe administrative"
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr "Ce projet est bloqué car son propriétaire est parti"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Texte"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Texte multi-ligne"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Date"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr "Url"
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "type"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "valeurs"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "histoire utilisateur"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "tâche"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "problème"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Un élément de même nom existe déjà"
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "réf"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "état"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "sujet"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "couleur"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "assigné à"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "est un requis client"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "est un requis de l'équipe"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Commentaire déjà supprimé"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Commentaire non supprimé"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Changement"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Créer"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Supprimer"
@@ -1587,7 +1637,7 @@ msgstr "supprimé"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Non assigné"
@@ -1634,95 +1684,75 @@ msgstr "De :"
msgid "To:"
msgstr "A :"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "contenu"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "note bloquée"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Vous n'avez pas la permission d'affecter ce sprint à ce problème."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "Vous n'avez pas la permission d'affecter cette sévérité à ce problème."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "Vous n'avez pas la permission d'affecter cette priorité à ce problème."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Vous n'avez pas la permission d'affecter ce type à ce problème."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "réf"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "état"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "sévérité"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "priorité"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "jalon"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "date de fin"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "sujet"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "assigné à"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "référence externe"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Aimer"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Aime"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -1734,8 +1764,9 @@ msgstr "date de démarrage estimée"
msgid "estimated finish date"
msgstr "date de fin estimée"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "est fermé"
@@ -1747,290 +1778,384 @@ msgstr "disponibilité"
msgid "The estimated start must be previous to the estimated finish."
msgstr "La date de démarrage doit être antérieure à la de fin prévisionnelle"
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Il n'y a pas de sprint avec cet id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "est bloqué"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' paramètre obligatoire"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project' paramètre obligatoire"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "Créé le"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "jeton"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "Text supplémentaire de l'invitation"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "classement utilisateur"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "L'utilisateur est déjà un membre du projet"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "Points par défaut"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "statut de l'HU par défaut"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "Points par défaut"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "Etat par défaut des tâches"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "Priorité par défaut"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "Sévérité par défaut"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "statut du problème par défaut"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "type de problème par défaut"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membres"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total des jalons"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "total des points d'histoire"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "panneau backlog actif"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "panneau kanban actif"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "panneau wiki actif"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "panneau problèmes actif"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "plateforme de vidéoconférence"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "données complémentaires pour la salle de vidéoconférence"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "Modèle de création"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "Permissions anonymes"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "Permission de l'utilisateur"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "est privé"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "Permissions anonymes"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "Permission de l'utilisateur"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr "est mis en avant"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr "est à la recherche de main d'oeuvre"
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "couleurs des tags"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr "jeton de transfert de projet"
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr "code bloqué"
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "date de mise à jour"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "total"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr "fans la semaine dernière"
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr "fans le mois dernier"
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr "fans l'année dernière"
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr "activité de la semaine écoulée"
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr "activité du mois écoulé"
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr "activité de l'année écoulée"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "Configurations des modules"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "est archivé"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "couleur"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "limite de travail en cours"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "valeur"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "rôle par défaut du propriétaire"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "options par défaut"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "statuts des us"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "points"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "états des tâches"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "statuts des problèmes"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "types de problèmes"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "priorités"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "sévérités"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "rôles"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Impliqué"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Toutes"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Aucun"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "date de création"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "entrées dans l'historique"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "notifier les utilisateurs"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Suivre"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "La notification existe pour l'utilisateur et le projet spécifiés"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Valeur non valide pour le niveau de notification"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2532,162 +2657,181 @@ msgstr ""
"\n"
"[%(project)s] Page Wiki \"%(page)s\" supprimée\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "La liste des observateurs contient des utilisateurs invalides"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "La version doit être un nombre entier"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "La version n'est pas valide"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "La version ne correspond pas à la version courante"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "version"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
"Vous ne pouvez pas quitter le projet si vous en êtes le propriétaire ou "
"qu'il n'y a pas d'autre administrateur."
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Adresse email déjà existante"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Rôle non valide pour le projet"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
-msgstr "Le propriétaire du projet doit être un administrateur."
-
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-"Au moins un utilisateur doit être un administrateur actif de ce projet."
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Options par défaut"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Etats de la User Story"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Points"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Etats des tâches"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Statuts des problèmes"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Types de problèmes"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Priorités"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Sévérités"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Rôles"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets privés"
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets publics"
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr "Vous avez atteint le nombre maximum de projets privés"
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr "Ce projet privé est le dernier que vous pouvez rejoindre"
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr "Vous avez atteint le nombre maximum de projets publics."
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr "Ce projet public est le dernier que vous pouvez rejoindre"
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Sprint futurs"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Fin du projet"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Jeton invalide"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr "Le jeton est périmé"
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "tags"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "couleurs des tags"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "Vous n'avez pas la permission d'affecter ce sprint à cette tâche."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr "Vous n'avez pas la permission d'affecter ce récit à cette tâche."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "ordre des us"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "order du tableau de tâches"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "est de l'iocaine"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Il n'existe pas de tâche avec cet identifant"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3075,12 +3219,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3098,12 +3242,12 @@ msgstr ""
"sur le produit et sur ses utilisateurs."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3117,305 +3261,391 @@ msgstr ""
"qui peuvent le consulter et y puiser leur travail."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Nouveau"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Prêt"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "En cours"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Prêt à tester"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Fait"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Archivé"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Fermé"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Infos manquantes"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Repoussé"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Rejeté"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Bug"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Question"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Amélioration"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Faible"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Fort"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Souhaits"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Mineur"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Important"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Critique"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "Expérience utilisateur"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Product Owner"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Participant"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Vous n'avez pas la permission d'affecter ce sprint à ce récit utilisateur."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Vous n'avez pas la permission d'affecter ce statut à ce récit utilisateur."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rôle"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "order du backlog"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "ordre du sprint"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "date de fin"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "est un requis client"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "est un requis de l'équipe"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "généré depuis un problème"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Il n'y a pas d'user story avec cet id"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Aucun projet avec cet identifiant"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Il n'y a pas de statut d'user story avec cet id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Adresse email déjà existante"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Il n'y a pas de statut de tâche avec cet id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Rôle non valide pour le projet"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr "Le propriétaire du projet doit être un administrateur."
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+"Au moins un utilisateur doit être un administrateur actif de ce projet."
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Options par défaut"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Etats de la User Story"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Points"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Etats des tâches"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Statuts des problèmes"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Types de problèmes"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Priorités"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Sévérités"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Rôles"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Votes"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "vote"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'content' paramètre obligatoire"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' paramètre obligatoire"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "dernier modificateur"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr "id"
@@ -3443,54 +3673,54 @@ msgstr "Restrictions"
msgid "Important dates"
msgstr "Dates importantes"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Email dupliquée"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Email non valide"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Nom d'utilisateur ou email non valide"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Mail envoyé avec succès!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Paramètre 'mot de passe actuel' requis"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Paramètre 'nouveau mot de passe' requis"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Le mot de passe doit être d'au moins 6 caractères"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Mot de passe actuel incorrect"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Invalide, êtes-vous sûre que le jeton est correct et qu'il n'a pas déjà été "
"utilisé ?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Invalide, êtes-vous sûre que le jeton est correct ?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "statut superutilisateur"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3498,25 +3728,25 @@ msgstr ""
"Indique que l'utilisateur a toutes les permissions sans avoir à lui les "
"donner explicitement"
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "nom d'utilisateur"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Obligatoire. 30 caractères maximum. Lettres, nombres et les caractères /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Entrez un nom d'utilisateur valide"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "actif"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3524,71 +3754,63 @@ msgstr ""
"Indique qu'un utilisateur est considéré ou non comme actif. Désélectionnez "
"cette option au lieu de supprimer le compte utilisateur."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biographie"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "photo"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "date d'inscription"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "langage par défaut"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "thème par défaut"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "Fuseau horaire par défaut"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "changer la couleur des tags"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "jeton email"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "nouvelle adresse email"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "permissions"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "invalide"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Aucun utilisateur avec ce nom ou ce mot de passe."
@@ -3772,47 +3994,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Vous avez été Taigarisés!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Aucun rôle avec cet identifiant"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "invalide"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr "Violation de clé primaire. La clé '{}' existe déjà."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "clé"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "clé secrète"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "code retour"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "données de la requête"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "en-têtes de la requête"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "données de la réponse"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "en-têtes de la réponse"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "durée"
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index 8090f674..b1b12bc8 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -15,8 +15,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@@ -26,156 +26,160 @@ msgstr ""
"Language: it\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "La registrazione pubblica è disabilitata."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "Tipo di registrazione non valida"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "Tipo di login non valido"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Il nome utente scelto è già utilizzato."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "L'email inserita è già utilizzata."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Il token non corrisponde a nessun invito valido."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "L'Utente è già registrato."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "Questo utente fa già parte del progetto."
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Errore nella creazione della nuova utenza."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Token non valido"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "Nome utente non valido"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
"Obbligatorio. Al massimo 255 caratteri, Contenenti: lettere, numeri e "
"caratteri /./-/_ "
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Il nome utente scelto è già utilizzato."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "L'email inserita è già utilizzata."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Il token non corrisponde a nessun invito valido."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "L'Utente è già registrato."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "Questo utente fa già parte del progetto."
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Errore nella creazione della nuova utenza."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Token non valido"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Questo campo è obbligatorio."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Valore non valido."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "il valore di '%s' deve essere o Vero o Falso."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Uno 'slug' valido è composto da lettere, numeri, caratteri di sottolineatura "
"o trattini."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Seleziona una scelta valida. %(value)s non è fra le scelte disponibili."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Inserisci un indirizzo e-mail valido."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "La data non ha un formato valido. Usa uno dei formati disponibili: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "L'orario non ha un formato valido. Usa uno dei formati disponibili: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "Formato temporale errato. Usa uno dei seguenti formati: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Inserisci il numero completo."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Assicurati che questo valore sia minore o uguale di %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Assicurati che questo valore sia maggiore o uguale di %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "il valore \"%s\" deve essere un valore \"float\"."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Inserisci un numero."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Assicurati che non ci siano più di %s cifre in totale."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Assicurati che non ci siano più di %s decimali."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Assicurati che non ci siano più di %s cifre prima del punto decimale."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr ""
"Non è stato caricato alcun file. Controlla il tipo di codifica nella scheda."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Nessun file caricato."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Il file caricato è vuoto."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -183,12 +187,12 @@ msgstr ""
"Assicurati che il nome del file abbia al massimo %(max)d caratteri (ne ha "
"%(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Carica il file oppure controlla la casella deselezionata. Non entrambi. "
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -196,183 +200,180 @@ msgstr ""
"Carica un'immagina valida. Il file caricato potrebbe non essere un'immagine "
"o l'immagine potrebbe essere corrotta. "
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "La pagina non è 'last', né può essere convertita come int."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Pagina (%(page_number)s) invalida: %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Definizione di permesso non valida."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "pk '%s' invalido - l'oggetto non esiste"
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Inserimento scorretto. Atteso un valore pk, ricevuto %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "L'oggetto con %s=%s non esiste."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Hyperlink invalido - nessun URL abbinato"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Hyperlink invalido - l'URL abbinato non è corretto"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "URL invalido a causa di un errore di configurazione"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Hyperlink invalido - l'oggetto non esiste"
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Inserimento scorretto. Attesa una stringa con URL, ricevuto %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Dati non validi"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Non è stato fornito nessun input"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Non è possibile creare un nuovo elemento, solo quelli esistenti possono "
"essere aggiornati"
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Ci si aspetta una lista di oggetti."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Non trovato"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Permesso negato"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Errore sul server"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Errore di connessione"
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Richiesta composta erroneamente."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Le credenziali non sono corrette."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Le credenziali per l'autenticazione non sono state fornite."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Non hai il permesso per eseguire l'azione. "
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Metodo '%s' non permesso."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr ""
"Non è possibile soddisfare la richiesta di accettazione dell'intestazione."
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Nella richiesta è presente un contenuto media '%s' non supportato."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "La richiesta è stata soppressa"
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Disponibile in %d secondi%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Errore inaspettato"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Non trovato."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Metodo non supportato dall'endpoint."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Argomento errato."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Errore di validazione dei dati"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Errore di integrità causato da un argomento invalido o sbagliato"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Errore di precondizione"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Errore nel filtro del tipo di parametri."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'Progetto' deve essere un valore intero."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "tags"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -427,7 +428,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -439,33 +440,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-"Supporto Taiga:\n"
-"\n"
-""
-"%(support_url)s\n"
-"\n"
-"
\n"
-"\n"
-"Contact us:\n"
-"\n"
-"\n"
-"\n"
-"%(support_email)s\n"
-"\n"
-"\n"
-"\n"
-"
\n"
-"\n"
-"Mailing list:\n"
-"\n"
-"\n"
-"\n"
-"%(mailing_list_url)s\n"
-"\n"
-""
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -519,104 +493,89 @@ msgstr ""
"\n"
"Commento: %(comment)s"
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "C'è bisogno di almeno un ruolo"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "E' richiesto un file di dump"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Formato di dump invalido"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" non è stato trovato in questo progetto"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Contenuto errato. Deve essere {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Contiene campi personalizzati invalidi."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Il nome del progetto è duplicato"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "Errore nell'importazione del progetto dati"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "Errore nell'importazione i ruoli"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "Errore nell'importazione delle iscrizioni"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "Errore nell'importazione della lista degli attributi di progetto"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr ""
"Errore nell'importazione dei valori predefiniti degli attributi del progetto."
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "Errore nell'importazione degli attributi personalizzati"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "errore nell'importazione degli sprints"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "Errore nell'importazione delle user story"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "Errore nell'importazione dei compiti "
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "errore nell'importazione dei problemi"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "Errore nell'importazione delle user story"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "Errore nell'importazione dei compiti "
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "Errore nell'importazione delle pagine wiki"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "Errore nell'importazione dei link di wiki"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "Errore nell'importazione dei tags"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "Errore nell'importazione delle timelines"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Errore nella creazione del dump di progetto"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -636,15 +595,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Errore nel caricamento del dump di progetto"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -942,77 +901,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Il dump del tuo progetto è stato importato"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" non è stato trovato in questo progetto"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Contenuto errato. Deve essere {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Contiene campi personalizzati invalidi."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Il nome del progetto è duplicato"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "E' richiesta l'autenticazione"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "nome"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "Url dell'icona"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "descrizione"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Url successivo"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "chiave segreta per cifrare i token dell'applicazione"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "utente"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "applicazione"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "Nome completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "Inserisci un indirizzo e-mail valido."
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "Commento"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "data creata"
@@ -1042,7 +1021,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informazioni aggiuntive"
@@ -1082,565 +1061,577 @@ msgstr ""
"\n"
"[Taiga] Hai un feedback da %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "Il carico non è un json valido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Il progetto non esiste"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Firma non valida"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "L'elemento di riferimento non esiste"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Lo stato non esiste"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Lo stato è stato modificato a seguito di un commit di BitBucket"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Informazione sul problema non valida"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-"Problema creato da [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") da BitBucket.\n"
-"\n"
-"Origine del problema su BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Go to 'bb#{number} - {subject}'\"):\n"
-"\n"
-"\n"
-"\n"
-"{description}"
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Problema creato da BItBucket"
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Commento sul problema non valido"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-"Commento da [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") da BitBucket.\n"
-"\n"
-"Origine del problema da BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Go to 'bb#{number} - {subject}'\")\n"
-"\n"
-"\n"
-"\n"
-"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Informazione sul problema non valida"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Commento da BitBucket:\n"
-"\n"
-"\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Stato cambiato da [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Lo stato è stato modificato da un commit su GitHub."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"Problema creato da [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") su GitHub.\n"
-"\n"
-"Origine del problema su GitHub: [gh#{number} - {subject}]({github_url} \"Go "
-"to 'gh#{number} - {subject}'\"):\n"
-"\n"
-"\n"
-"\n"
-"{description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Problema creato su GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-"Commento da [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") su GitHub.\n"
-"Origine del problema su GitHub: [gh#{number} - {subject}]({github_url} \"Go "
-"to 'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Commento su GitHub:\n"
-"\n"
-"\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "L'elemento di riferimento non esiste"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Lo stato è stato modificato tramite commit su GitLab"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Lo stato non esiste"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Creato da GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Commento da [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") su GitLab.\n"
-"\n"
-"Origine del problema su GitLab: [gl#{number} - {subject}]({gitlab_url} \"Go "
-"to 'gl#{number} - {subject}'\")\n"
-"\n"
-"\n"
-"\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Commento da GitLab:\n"
-"\n"
-"\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Vedi progetto"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Guarda le milestones"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Guarda le storie utente"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Guarda i compiti"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Guarda i problemi"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Guarda le pagine wiki"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Guarda i lik di wiki"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Richiedi l'iscrizione"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Aggiungi una storia utente al progetto"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Aggiungi dei commenti alle storia utente"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Aggiungi dei commenti ai compiti"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Aggiungi i problemi"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Aggiungi dei commenti ai problemi"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Aggiungi una pagina wiki"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Modifica la pagina wiki"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Aggiungi un link wiki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modifica il link di wiki"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Aggiungi una tappa"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modifica la tappa"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Elimina la tappa"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Guarda la storia utente"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Aggiungi una storia utente"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Modifica una storia utente"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Cancella una storia utente"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Aggiungi un compito"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modifica il compito"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Elimina compito"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Aggiungi un problema"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Modifica il problema"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Elimina il problema"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Aggiungi una pagina wiki"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Modifica la pagina wiki"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Elimina la pagina wiki"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Aggiungi un link wiki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modifica il link di wiki"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Elimina la pagina wiki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Modifica il progetto"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Aggiungi un membro"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Rimuovi il membro"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Elimina il progetto"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Aggiungi un membro"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Rimuovi il membro"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Valori dell'amministratore del progetto"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Ruoli dell'amministratore"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "proprietario"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Argomento non valido"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Formato dell'immagine non valido"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Il nome del template non è valido"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "La descrizione del template non è valida"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Non hai il permesso di vedere questo elemento."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Aggiornamento non parziale non supportato"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "L'ID di progetto non corrisponde tra oggetto e progetto"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "progetto"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "tipo di contenuto"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "ID dell'oggetto"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "data modificata"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "file allegato"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "non approvato"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "ordine"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "ApparIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Personalizzato"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Testo"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Testo multi-linea"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Data"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "tipo"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "valori"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "storia utente"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "compito"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "problema"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Ne esiste già un altro con lo stesso nome"
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "referenza"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "stato"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "soggeto"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "colore"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "assegnato a"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "é un requisito del cliente "
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "é una richiesta del team"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Il commento è già stato eliminato"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Commento non eliminato"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Cambiato"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Creato"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Eliminato"
@@ -1696,7 +1687,7 @@ msgstr "rimosso"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Non assegnato"
@@ -1743,95 +1734,75 @@ msgstr "Da:"
msgid "To:"
msgstr "A:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "contenuto"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "nota bloccata"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Non hai i permessi per aggiungere questo sprint a questo problema"
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Non hai i permessi per aggiungere questo stato a questo problema"
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "Non hai i permessi per aggiungere questa criticità a questo problema"
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "Non hai i permessi per aggiungere questa priorità a questo problema."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Non hai i permessi per aggiungere questa tipologia a questo problema"
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "referenza"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "stato"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "criticità"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "priorità"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "tappa"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "data di conclusione"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "soggeto"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "assegnato a"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "referenza esterna"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Like"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Piaciuto"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "lumaca"
@@ -1843,8 +1814,9 @@ msgstr "data stimata di inizio"
msgid "estimated finish date"
msgstr "data stimata di fine"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "è concluso"
@@ -1857,290 +1829,384 @@ msgid "The estimated start must be previous to the estimated finish."
msgstr ""
"La data stimata di inizio deve essere precedente alla data stimata di fine."
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Non c'è nessuno sprint on questo ID"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "è bloccato"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "il parametro '{param}' è obbligatorio"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "il parametro 'project' è obbligatorio"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "creato a "
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "testo ulteriore per l'invito"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "ordine dell'utente"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "L'utente è già membro del progetto"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "punti predefiniti"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "stati predefiniti per le storie utente"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "punti predefiniti"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "stati predefiniti del compito"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "priorità predefinita"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "criticità predefinita"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "stato predefinito del problema"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "tipologia predefinita del problema"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membri"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "tappe totali"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "punti totali della storia"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "pannello di backlog attivo"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "pannello kanban attivo"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "pannello wiki attivo"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "pannello dei problemi attivo"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "sistema di videoconferenza"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "ulteriori dati di videoconferenza "
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "creazione del template"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "permessi anonimi"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "permessi dell'utente"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "è privato"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "permessi anonimi"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "permessi dell'utente"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr "in vetrina"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr "sta cercando persone"
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr "note sulla ricerca delle persone "
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "colori dei tag"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "tempo e data aggiornati"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "conta"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr "fans nella settimana"
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr "fans nel mese"
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr "fans nell'anno"
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr "attività nella settimana"
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr "attività nel mese"
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr "attività nell'anno"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "configurazione dei moduli"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "è archivitato"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "colore"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "limite dei lavori in corso"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "valore"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "ruolo proprietario predefinito"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "opzioni predefinite "
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "stati della storia utente"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "punti"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "stati del compito"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "stati del probema"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "tipologie del problema"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "priorità"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "criticità "
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "ruoli"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Coinvolto"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Tutti"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Nessuno"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "tempo e data creati"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "inserimenti della storia"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "notifica utenti"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Osservato"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "La notifica esiste per l'utente e il progetto specificati"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Valore non valido per il livello di notifica"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2998,160 +3064,180 @@ msgstr ""
"\n"
"[%(project)s] ha eliminato la pagina wiki \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "L'osservatore contiene un utente non valido"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "La versione deve essere un intero"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "Il parametro della versione non è valido"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "La versione non corrisponde a quella corrente"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "versione"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "L'indirizzo email è già usato"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Ruolo di progetto non valido"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Opzioni predefinite"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Stati della storia utente"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Punti"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Stati del compito"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Stati del problema"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Tipologie del problema"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Priorità"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Criticità"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Ruoli"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Sprint futuri"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Termine di progetto"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Token non valido"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "tags"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "colori dei tag"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "Non hai i permessi per aggiungere questo sprint a questo compito."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Non hai i permessi per aggiungere questa storia utente a questo compito."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "Non hai i permessi per aggiungere questo stato a questo compito."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "ordine della storia utente"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "ordine del pannello dei compiti"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "è sotto aspirina"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Non c'è nessun compito con questo ID"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3562,12 +3648,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3584,12 +3670,12 @@ msgstr ""
"caratteristiche del prodotto e dei suoi clienti."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3603,304 +3689,389 @@ msgstr ""
"membri del team, in modo che possano organizzare il lavoro."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Nuovo"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Pronto"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "In via di sviluppo"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Pronto per il test"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Fatto"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Archiviato"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Concluso"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Necessita di informazioni"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Postposto "
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Rifiutato"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Bug"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Domanda"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Miglioramento"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Basso"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normale"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Alto"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Lista dei desideri"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Minore"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Importante"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Critico"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Product Owner"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Non hai i permessi per aggiungere questo sprint a questa storia utente."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr "Non hai i permessi per aggiungere questo stato a questa storia utente."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Stiamo generando la storia utente #{ref} - {subject}"
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "ruolo"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "ordine del backlog"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "ordine dello sprint"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "data di termine"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "é un requisito del cliente "
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "é una richiesta del team"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "generato da un problema"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Non c'è nessuna storia utente con questo ID"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Non c'è nessuno progetto con questo ID"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Non c'è nessuno stato della storia utente con questo ID"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "L'indirizzo email è già usato"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Non c'è nessuno stato del compito con questo ID"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Ruolo di progetto non valido"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Opzioni predefinite"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Stati della storia utente"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Punti"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Stati del compito"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Stati del problema"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Tipologie del problema"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Priorità"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Criticità"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Ruoli"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Voti"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Voto"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "il parametro 'contenuto' è obbligatorio"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "Il parametro 'ID progetto' è obbligatorio"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "ultima modificatore"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Controlla le API della storie per la differenza esatta"
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3928,54 +4099,54 @@ msgstr ""
msgid "Important dates"
msgstr "Date importanti"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "E-mail duplicata"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "E-mail non valida"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Username o e-mail non validi"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Mail inviata con successo!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "E' necessario il parametro della password corrente"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "E' necessario il parametro della nuovo password"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Lunghezza della password non valida, sono necessari almeno 6 caratteri"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Password corrente non valida"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Non valido. Sei sicuro che il token sia corretto e che tu non l'abbia già "
"usato in precedenza?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Non valido. Sicuro che il token sia corretto?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "Stato del super-utente"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3983,26 +4154,26 @@ msgstr ""
"Definisce che questo utente ha tutti i permessi senza assegnarglieli "
"esplicitamente."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "nome utente"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Richiede 30 caratteri o meno. Deve comprendere: lettere, numeri e caratteri "
"come /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Inserisci un nome utente valido."
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "attivo"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -4010,71 +4181,63 @@ msgstr ""
"Definisce se questo utente debba essere trattato come attivo. Deseleziona "
"questo invece di eliminare gli account."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "fotografia"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "data di inizio partecipazione"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "lingua predefinita"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "tema predefinito"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "timezone predefinita"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "colora i tag"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "token e-mail"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "nuovo indirizzo e-mail"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "permessi"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "non valido"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Nome utente non valido. Provane uno diverso."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Il nome utente o la password non corrispondono all'utente."
@@ -4302,49 +4465,53 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Sei stato Taigazzato!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Non c'è nessuno ruolo con questo ID"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "non valido"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Nome utente non valido. Provane uno diverso."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
"Un valore di chiave duplicato viola il vincolo unico. La chiave '{}' esiste "
"già."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "chiave"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "chiave segreta"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "codice di stato"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "dati della richiesta"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "header della richiesta"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "dati della risposta"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "header della risposta"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "durata"
diff --git a/taiga/locale/nb/LC_MESSAGES/django.po b/taiga/locale/nb/LC_MESSAGES/django.po
new file mode 100644
index 00000000..0524d9ef
--- /dev/null
+++ b/taiga/locale/nb/LC_MESSAGES/django.po
@@ -0,0 +1,3804 @@
+# taiga-back.taiga.
+# Copyright (C) 2014-2016 Taiga Dev Team
+# This file is distributed under the same license as the taiga-back package.
+#
+# Translators:
+# Jørgen Skår Fischer , 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: taiga-back\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
+"Last-Translator: Taiga Dev Team \n"
+"Language-Team: Norwegian Bokmål (http://www.transifex.com/taiga-agile-llc/"
+"taiga-back/language/nb/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nb\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: taiga/auth/api.py:102
+msgid "Public register is disabled."
+msgstr "Offentlig register er deaktivert."
+
+#: taiga/auth/api.py:135
+msgid "invalid register type"
+msgstr "ugyldig registertype"
+
+#: taiga/auth/api.py:148
+msgid "invalid login type"
+msgstr "ugyldig påloggingstype"
+
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Brukernavnet er allerede i bruk."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Epostadressen er allerede i bruk."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Poletten samvsarer ikke med noen gyldig invitasjon."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Brukeren er allerede registrert."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "Denne brukeren er allerede et medlem i prosjektet."
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Feil ved å lage ny bruker."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Ugyldig polett"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
+msgid "invalid username"
+msgstr "ugyldig brukernavn"
+
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
+msgid ""
+"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
+msgstr "Påkrevd. 255 tegn eller færre. Bokstaver, tall og /./-/_ tegn '"
+
+#: taiga/base/api/fields.py:294
+msgid "This field is required."
+msgstr "Dette feltet er obligatorisk."
+
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
+msgid "Invalid value."
+msgstr "Ugyldig verdi."
+
+#: taiga/base/api/fields.py:479
+#, python-format
+msgid "'%s' value must be either True or False."
+msgstr "'%s' verdi må være enten 'True' eller 'False'"
+
+#: taiga/base/api/fields.py:543
+msgid ""
+"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
+msgstr ""
+"Skriv inn en gyldig 'slug' bestående av bokstaver, tall, understreker eller "
+"bindestreker. "
+
+#: taiga/base/api/fields.py:558
+#, python-format
+msgid "Select a valid choice. %(value)s is not one of the available choices."
+msgstr "Gjør et gyldig valg. %(value)s er ikke et av de tilgjengelige valgene."
+
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
+msgid "Enter a valid email address."
+msgstr "Skriv inn en gyldig epostadresse."
+
+#: taiga/base/api/fields.py:672
+#, python-format
+msgid "Date has wrong format. Use one of these formats instead: %s"
+msgstr "Datoen har feil format. Bruk en av disse formatene istedet: %s"
+
+#: taiga/base/api/fields.py:736
+#, python-format
+msgid "Datetime has wrong format. Use one of these formats instead: %s"
+msgstr "Datotid har feil format. Bruk en av disse formatene istedet: %s"
+
+#: taiga/base/api/fields.py:806
+#, python-format
+msgid "Time has wrong format. Use one of these formats instead: %s"
+msgstr "Tid har feil format. Bruk en av disse formatene istedet: %s"
+
+#: taiga/base/api/fields.py:863
+msgid "Enter a whole number."
+msgstr "Skriv inn et heltall."
+
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
+#, python-format
+msgid "Ensure this value is less than or equal to %(limit_value)s."
+msgstr "Sikre at denne verdien er mindre enn eller lik %(limit_value)s."
+
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
+#, python-format
+msgid "Ensure this value is greater than or equal to %(limit_value)s."
+msgstr "Sikre at denne verdien er større enn eller lik %(limit_value)s."
+
+#: taiga/base/api/fields.py:895
+#, python-format
+msgid "\"%s\" value must be a float."
+msgstr "\"%s\" verdi må være et desimaltall."
+
+#: taiga/base/api/fields.py:916
+msgid "Enter a number."
+msgstr "Skriv inn et nummer."
+
+#: taiga/base/api/fields.py:919
+#, python-format
+msgid "Ensure that there are no more than %s digits in total."
+msgstr "Pass på at det ikke er flere enn %s sifre totalt."
+
+#: taiga/base/api/fields.py:920
+#, python-format
+msgid "Ensure that there are no more than %s decimal places."
+msgstr "Pass på at det ikke er flere enn %s desimaler."
+
+#: taiga/base/api/fields.py:921
+#, python-format
+msgid "Ensure that there are no more than %s digits before the decimal point."
+msgstr "Pass på at det ikke er flere enn %s siffer før komma."
+
+#: taiga/base/api/fields.py:988
+msgid "No file was submitted. Check the encoding type on the form."
+msgstr "Ingen fil ble sendt. Kontroller kodingstypen på skjemaet."
+
+#: taiga/base/api/fields.py:989
+msgid "No file was submitted."
+msgstr "Ingen fil ble sendt."
+
+#: taiga/base/api/fields.py:990
+msgid "The submitted file is empty."
+msgstr "Den sendte filen er tom."
+
+#: taiga/base/api/fields.py:991
+#, python-format
+msgid ""
+"Ensure this filename has at most %(max)d characters (it has %(length)d)."
+msgstr ""
+"Sikre at dette filnavnet har på det meste %(max)d tegn (det har %(length)d)."
+
+#: taiga/base/api/fields.py:992
+msgid "Please either submit a file or check the clear checkbox, not both."
+msgstr ""
+"Vennligst enten send inn en fil eller sjekk den klare sjekkboksen, ikke "
+"begge deler."
+
+#: taiga/base/api/fields.py:1032
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+"Last opp et gyldig bilde. Filen du lastet opp var enten ikke et bilde eller "
+"et ødelagt bilde."
+
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
+msgid "Blocked element"
+msgstr "Blokkert element"
+
+#: taiga/base/api/pagination.py:214
+msgid "Page is not 'last', nor can it be converted to an int."
+msgstr ""
+"Siden er ikke 'sist', og den kan heller ikke konverteres til en integer."
+
+#: taiga/base/api/pagination.py:218
+#, python-format
+msgid "Invalid page (%(page_number)s): %(message)s"
+msgstr "Ugyldig side (%(page_number)s): %(message)s"
+
+#: taiga/base/api/permissions.py:66
+msgid "Invalid permission definition."
+msgstr "Ugyldig tilgangsdefinisjon."
+
+#: taiga/base/api/relations.py:247
+#, python-format
+msgid "Invalid pk '%s' - object does not exist."
+msgstr "Ugyldig pk '%s' - objektet eksisterer ikke."
+
+#: taiga/base/api/relations.py:248
+#, python-format
+msgid "Incorrect type. Expected pk value, received %s."
+msgstr "Feil type. Forventet \"pk\" verdi, mottok %s."
+
+#: taiga/base/api/relations.py:336
+#, python-format
+msgid "Object with %s=%s does not exist."
+msgstr "Objekt med %s=%s eksisterer ikke"
+
+#: taiga/base/api/relations.py:372
+msgid "Invalid hyperlink - No URL match"
+msgstr "Ugyldig "
+
+#: taiga/base/api/relations.py:373
+msgid "Invalid hyperlink - Incorrect URL match"
+msgstr "Ugyldig hyperkobling - Feil URL"
+
+#: taiga/base/api/relations.py:374
+msgid "Invalid hyperlink due to configuration error"
+msgstr "Ugyldig hyperkobling på grunn av konfigurasjonsfeil"
+
+#: taiga/base/api/relations.py:375
+msgid "Invalid hyperlink - object does not exist."
+msgstr "Ugyldig hyperkobling - objekt finnes ikke."
+
+#: taiga/base/api/relations.py:376
+#, python-format
+msgid "Incorrect type. Expected url string, received %s."
+msgstr "Feil type. Forventet url streng, fikk %s."
+
+#: taiga/base/api/serializers.py:324
+msgid "Invalid data"
+msgstr "Ugyldig data."
+
+#: taiga/base/api/serializers.py:416
+msgid "No input provided"
+msgstr "Ingen inndata ble angitt"
+
+#: taiga/base/api/serializers.py:579
+msgid "Cannot create a new item, only existing items may be updated."
+msgstr ""
+"Kan ikke opprette et nytt element, kun eksisterende elementer kan oppdateres."
+
+#: taiga/base/api/serializers.py:590
+msgid "Expected a list of items."
+msgstr "Forventet en liste med elementer."
+
+#: taiga/base/api/views.py:126
+msgid "Not found"
+msgstr "Ikke funnet"
+
+#: taiga/base/api/views.py:129
+msgid "Permission denied"
+msgstr "Tilgang nektet"
+
+#: taiga/base/api/views.py:477
+msgid "Server application error"
+msgstr "Server programfeil"
+
+#: taiga/base/connectors/exceptions.py:26
+msgid "Connection error."
+msgstr "Tilkoblingsfeil"
+
+#: taiga/base/exceptions.py:79
+msgid "Malformed request."
+msgstr "Uriktig formatert forespørsel"
+
+#: taiga/base/exceptions.py:84
+msgid "Incorrect authentication credentials."
+msgstr "Feil godkjenningsinformasjon."
+
+#: taiga/base/exceptions.py:89
+msgid "Authentication credentials were not provided."
+msgstr "Autentiseringsopplysninger ble ikke gitt."
+
+#: taiga/base/exceptions.py:94
+msgid "You do not have permission to perform this action."
+msgstr "Du har ikke tillatelse til å utføre denne handlingen."
+
+#: taiga/base/exceptions.py:99
+#, python-format
+msgid "Method '%s' not allowed."
+msgstr "Metode '%s' ikke tillatt."
+
+#: taiga/base/exceptions.py:107
+msgid "Could not satisfy the request's Accept header"
+msgstr "Kunne ikke tilfredsstille forespørselens 'Accept header'"
+
+#: taiga/base/exceptions.py:116
+#, python-format
+msgid "Unsupported media type '%s' in request."
+msgstr "Uegnet medietype '%s' i forespørselen."
+
+#: taiga/base/exceptions.py:124
+msgid "Request was throttled."
+msgstr "Forespørselen ble strupet."
+
+#: taiga/base/exceptions.py:125
+#, python-format
+msgid "Expected available in %d second%s."
+msgstr "Forventet tilgjengelig om %d second%s."
+
+#: taiga/base/exceptions.py:139
+msgid "Unexpected error"
+msgstr "Uventet feil"
+
+#: taiga/base/exceptions.py:151
+msgid "Not found."
+msgstr "Ikke funnet."
+
+#: taiga/base/exceptions.py:156
+msgid "Method not supported for this endpoint."
+msgstr "Metode ikke støttet for dette endepunktet ."
+
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
+msgid "Wrong arguments."
+msgstr "Feil argumenter."
+
+#: taiga/base/exceptions.py:176
+msgid "Data validation error"
+msgstr "Data valideringsfeil"
+
+#: taiga/base/exceptions.py:188
+msgid "Integrity Error for wrong or invalid arguments"
+msgstr "Integritetsfeil for gale eller ugyldige argumenter"
+
+#: taiga/base/exceptions.py:195
+msgid "Precondition error"
+msgstr "Forutsetningsfeil"
+
+#: taiga/base/exceptions.py:219
+msgid "No room left for more projects."
+msgstr "Ingen plass igjen til nye prosjekter."
+
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
+msgid "Error in filter params types."
+msgstr "Feil i filterparameter typer"
+
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
+msgid "'project' must be an integer value."
+msgstr "'project' må være et heltall"
+
+#: taiga/base/templates/emails/base-body-html.jinja:6
+msgid "Taiga"
+msgstr "Taiga"
+
+#: taiga/base/templates/emails/base-body-html.jinja:406
+#: taiga/base/templates/emails/hero-body-html.jinja:380
+#: taiga/base/templates/emails/updates-body-html.jinja:442
+msgid "Follow us on Twitter"
+msgstr "Følg oss på Twitter"
+
+#: taiga/base/templates/emails/base-body-html.jinja:406
+#: taiga/base/templates/emails/hero-body-html.jinja:380
+#: taiga/base/templates/emails/updates-body-html.jinja:442
+msgid "Twitter"
+msgstr "Twitter"
+
+#: taiga/base/templates/emails/base-body-html.jinja:407
+#: taiga/base/templates/emails/hero-body-html.jinja:381
+#: taiga/base/templates/emails/updates-body-html.jinja:443
+msgid "Get the code on GitHub"
+msgstr "Skaff koden på GitHub"
+
+#: taiga/base/templates/emails/base-body-html.jinja:407
+#: taiga/base/templates/emails/hero-body-html.jinja:381
+#: taiga/base/templates/emails/updates-body-html.jinja:443
+msgid "GitHub"
+msgstr "GitHub"
+
+#: taiga/base/templates/emails/base-body-html.jinja:408
+#: taiga/base/templates/emails/hero-body-html.jinja:382
+#: taiga/base/templates/emails/updates-body-html.jinja:444
+msgid "Visit our website"
+msgstr "Besøk vår webside"
+
+#: taiga/base/templates/emails/base-body-html.jinja:408
+#: taiga/base/templates/emails/hero-body-html.jinja:382
+#: taiga/base/templates/emails/updates-body-html.jinja:444
+msgid "Taiga.io"
+msgstr "Taiga.io"
+
+#: taiga/base/templates/emails/base-body-html.jinja:423
+#: taiga/base/templates/emails/hero-body-html.jinja:397
+#: taiga/base/templates/emails/updates-body-html.jinja:459
+#, python-format
+msgid ""
+"\n"
+" Taiga Support:"
+"strong>\n"
+" %(support_url)s\n"
+"
\n"
+" Contact us:"
+"strong>\n"
+" \n"
+" %(support_email)s\n"
+" \n"
+"
\n"
+" Mailing list:"
+"strong>\n"
+" \n"
+" %(mailing_list_url)s\n"
+" \n"
+" "
+msgstr ""
+
+#: taiga/base/templates/emails/hero-body-html.jinja:6
+msgid "You have been Taigatized"
+msgstr "Du har blitt Taigatisert"
+
+#: taiga/base/templates/emails/hero-body-html.jinja:359
+msgid ""
+"\n"
+" You have been Taigatized!"
+"
\n"
+" Welcome to Taiga, an Open "
+"Source, Agile Project Management Tool
\n"
+" "
+msgstr ""
+
+#: taiga/base/templates/emails/updates-body-html.jinja:6
+msgid "[Taiga] Updates"
+msgstr "[Taiga] Oppdateringer"
+
+#: taiga/base/templates/emails/updates-body-html.jinja:417
+msgid "Updates"
+msgstr "Oppdateringer"
+
+#: taiga/base/templates/emails/updates-body-html.jinja:423
+#, python-format
+msgid ""
+"\n"
+" comment:"
+"
\n"
+" "
+"%(comment)s
\n"
+" "
+msgstr ""
+"\n"
+" kommentar:"
+"
\n"
+" "
+"%(comment)s
\n"
+" "
+
+#: taiga/base/templates/emails/updates-body-text.jinja:6
+#, python-format
+msgid ""
+"\n"
+" Comment: %(comment)s\n"
+" "
+msgstr ""
+"\n"
+" Kommentar: %(comment)s\n"
+" "
+
+#: taiga/export_import/api.py:127
+msgid "We needed at least one role"
+msgstr "Vi trenger minst en rolle"
+
+#: taiga/export_import/api.py:323
+msgid "Needed dump file"
+msgstr "Har behov for dump-fil"
+
+#: taiga/export_import/api.py:333
+msgid "Invalid dump format"
+msgstr "Ugyldig fil-dump format"
+
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
+msgid "error importing project data"
+msgstr "feil under import av prosjektdata"
+
+#: taiga/export_import/services/store.py:743
+msgid "error importing roles"
+msgstr "feil under import av roller"
+
+#: taiga/export_import/services/store.py:748
+msgid "error importing memberships"
+msgstr "feil under import av medlemskap"
+
+#: taiga/export_import/services/store.py:759
+msgid "error importing lists of project attributes"
+msgstr "feil under import av prosjektegenskaper"
+
+#: taiga/export_import/services/store.py:763
+msgid "error importing default project attributes values"
+msgstr "feil under import av standard prosjektegenskapverdier"
+
+#: taiga/export_import/services/store.py:774
+msgid "error importing custom attributes"
+msgstr "feil under import av egendefinerte egenskaper"
+
+#: taiga/export_import/services/store.py:778
+msgid "error importing sprints"
+msgstr "feil under import av sprinter"
+
+#: taiga/export_import/services/store.py:782
+msgid "error importing issues"
+msgstr "feil ved import av hendelser"
+
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "feil ved import av brukerhistorier"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "feil ved import av oppgaver"
+
+#: taiga/export_import/services/store.py:798
+msgid "error importing wiki pages"
+msgstr "feil ved import av wiki-sider"
+
+#: taiga/export_import/services/store.py:802
+msgid "error importing wiki links"
+msgstr "feil ved import av wiki-lenker"
+
+#: taiga/export_import/services/store.py:806
+msgid "error importing tags"
+msgstr "feil ved import av etiketter"
+
+#: taiga/export_import/services/store.py:810
+msgid "error importing timelines"
+msgstr "feil ved import av tidslinjer"
+
+#: taiga/export_import/services/store.py:832
+msgid "unexpected error importing project"
+msgstr "uventet feil ved import av prosjekt"
+
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
+msgid "Error generating project dump"
+msgstr "Feil ved generering av prosjektet dump"
+
+#: taiga/export_import/tasks.py:91
+#, python-brace-format
+msgid ""
+"\n"
+"\n"
+"Error loading dump by {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"REASON:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"DETAILS:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"TRACE ERROR:\n"
+"------------"
+msgstr ""
+
+#: taiga/export_import/tasks.py:120
+msgid "Error loading project dump"
+msgstr "Feil ved lasting av prosjektet dump"
+
+#: taiga/export_import/tasks.py:121
+msgid "Error loading your project dump file"
+msgstr "Feil ved lasting av din prosjektdump-fil"
+
+#: taiga/export_import/tasks.py:135
+msgid " -- no detail info --"
+msgstr "-- ingen detaljeinfo --"
+
+#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Project dump generated
\n"
+" Hello %(user)s,
\n"
+" Your dump from project %(project)s has been correctly generated."
+"h3>\n"
+"
You can download it here:
\n"
+" Download the dump file\n"
+" This file will be deleted on %(deletion_date)s.
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/export_import/templates/emails/dump_project-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hello %(user)s,\n"
+"\n"
+"Your dump from project %(project)s has been correctly generated. You can "
+"download it here:\n"
+"\n"
+"%(url)s\n"
+"\n"
+"This file will be deleted on %(deletion_date)s.\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/export_import/templates/emails/dump_project-subject.jinja:1
+#, python-format
+msgid "[%(project)s] Your project dump has been generated"
+msgstr ""
+
+#: taiga/export_import/templates/emails/export_error-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" %(error_message)s
\n"
+" Hello %(user)s,
\n"
+" Your project %(project)s has not been exported correctly.
\n"
+" The Taiga system administrators have been informed.
Please, try "
+"it again or contact with the support team at\n"
+" %(support_email)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/export_import/templates/emails/export_error-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hello %(user)s,\n"
+"\n"
+"%(error_message)s\n"
+"Your project %(project)s has not been exported correctly.\n"
+"\n"
+"The Taiga system administrators have been informed.\n"
+"\n"
+"Please, try it again or contact with the support team at %(support_email)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/export_import/templates/emails/export_error-subject.jinja:1
+#, python-format
+msgid "[%(project)s] %(error_subject)s"
+msgstr ""
+
+#: taiga/export_import/templates/emails/import_error-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" %(error_message)s
\n"
+" Hello %(user)s,
\n"
+" Your project has not been importer correctly.
\n"
+" The Taiga system administrators have been informed.
Please, try "
+"it again or contact with the support team at\n"
+" %(support_email)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/export_import/templates/emails/import_error-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hello %(user)s,\n"
+"\n"
+"%(error_message)s\n"
+"\n"
+"Your project has not been importer correctly.\n"
+"\n"
+"The Taiga system administrators have been informed.\n"
+"\n"
+"Please, try it again or contact with the support team at %(support_email)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/export_import/templates/emails/import_error-subject.jinja:1
+#, python-format
+msgid "[Taiga] %(error_subject)s"
+msgstr ""
+
+#: taiga/export_import/templates/emails/load_dump-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Project dump imported
\n"
+" Hello %(user)s,
\n"
+" Your project dump has been correctly imported.
\n"
+" Go to %(project)s\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hello %(user)s,\n"
+"\n"
+"Your project dump has been correctly imported.\n"
+"\n"
+"You can see the project %(project)s here:\n"
+"\n"
+"%(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/export_import/templates/emails/load_dump-subject.jinja:1
+#, python-format
+msgid "[%(project)s] Your project dump has been imported"
+msgstr ""
+
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" ble ikke funnet i dette prosjektet"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Ugyldig innhold. Det må være {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Den inneholder ugyldige egendefinerte feilter"
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Navnet er duplisert for prosjektet"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
+msgid "Authentication required"
+msgstr "Autentisering kreves"
+
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
+msgid "name"
+msgstr "navn"
+
+#: taiga/external_apps/models.py:37
+msgid "Icon url"
+msgstr "Ikon url"
+
+#: taiga/external_apps/models.py:38
+msgid "web"
+msgstr "web"
+
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
+msgid "description"
+msgstr "beskrivelse"
+
+#: taiga/external_apps/models.py:41
+msgid "Next url"
+msgstr "Neste url"
+
+#: taiga/external_apps/models.py:43
+msgid "secret key for ciphering the application tokens"
+msgstr ""
+
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
+msgid "user"
+msgstr "bruker"
+
+#: taiga/external_apps/models.py:61
+msgid "application"
+msgstr "applikasjon"
+
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
+msgid "full name"
+msgstr "fullt navn"
+
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
+msgid "email address"
+msgstr "epostadresse"
+
+#: taiga/feedback/models.py:29
+msgid "comment"
+msgstr "kommentar"
+
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
+msgid "created date"
+msgstr "opprettet dato"
+
+#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Feedback
\n"
+" Taiga has received feedback from %(full_name)s <%(email)s>
\n"
+" "
+msgstr ""
+"\n"
+" Tilbakemelding
\n"
+" Taiga har mottatt tilbakemelding fra %(full_name)s <%(email)s>
\n"
+" "
+
+#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9
+#, python-format
+msgid ""
+"\n"
+" Comment
\n"
+" %(comment)s
\n"
+" "
+msgstr ""
+"\n"
+" Kommentar
\n"
+" %(comment)s
\n"
+" "
+
+#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
+msgid "Extra info"
+msgstr "Ekstra info"
+
+#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1
+#, python-format
+msgid ""
+"---------\n"
+"- From: %(full_name)s <%(email)s>\n"
+"---------\n"
+"- Comment:\n"
+"%(comment)s\n"
+"---------"
+msgstr ""
+"---------\n"
+"- Fra: %(full_name)s <%(email)s>\n"
+"---------\n"
+"- Kommentar:\n"
+"%(comment)s\n"
+"---------"
+
+#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8
+msgid "- Extra info:"
+msgstr "- Ekstra info: "
+
+#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[Taiga] Feedback from %(full_name)s <%(email)s>\n"
+msgstr ""
+"\n"
+"[Taiga] Tilbakemelding fra %(full_name)s <%(email)s>\n"
+
+#: taiga/hooks/api.py:54
+msgid "The payload is not a valid json"
+msgstr "Payloaden er ikke gyldig json"
+
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
+msgid "The project doesn't exist"
+msgstr "Prosjektet eksisterer ikke"
+
+#: taiga/hooks/api.py:66
+msgid "Bad signature"
+msgstr "Dårlig signatur"
+
+#: taiga/hooks/event_hooks.py:66
+#, python-brace-format
+msgid ""
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
+"\n"
+"\"{comment_message}\""
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:84
+msgid "Invalid issue comment information"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:103
+#, python-brace-format
+msgid ""
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Ugyldig hendelsesinformasjon"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
+#, python-brace-format
+msgid ""
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:161
+#, python-brace-format
+msgid ""
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:179
+#, python-brace-format
+msgid ""
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:184
+#, python-brace-format
+msgid ""
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Det refererte elementet finnes ikke"
+
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Statusen eksisterer ikke"
+
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
+msgid "View project"
+msgstr "Vis prosjekt"
+
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
+msgid "View milestones"
+msgstr "Vis milepæler"
+
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
+msgid "View user stories"
+msgstr "Vis brukerhistorier"
+
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
+msgid "View tasks"
+msgstr "Vis oppgaver"
+
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
+msgid "View issues"
+msgstr "Vis hendelser"
+
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
+msgid "View wiki pages"
+msgstr "Se wiki-sider"
+
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
+msgid "View wiki links"
+msgstr "Se wiki-lenker"
+
+#: taiga/permissions/choices.py:37
+msgid "Add milestone"
+msgstr "Legg til milepæl"
+
+#: taiga/permissions/choices.py:38
+msgid "Modify milestone"
+msgstr "Endre milepæl"
+
+#: taiga/permissions/choices.py:39
+msgid "Delete milestone"
+msgstr "Slett milepæl"
+
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
+msgid "View user story"
+msgstr "Vis brukerhistorie"
+
+#: taiga/permissions/choices.py:48
+msgid "Add user story"
+msgstr "Legg til brukerhistorie"
+
+#: taiga/permissions/choices.py:49
+msgid "Modify user story"
+msgstr "Rediger brukerhistorie"
+
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
+msgid "Delete user story"
+msgstr "Slett brukerhistorie"
+
+#: taiga/permissions/choices.py:54
+msgid "Add task"
+msgstr "Legg til oppgave"
+
+#: taiga/permissions/choices.py:55
+msgid "Modify task"
+msgstr "Rediger oppgave"
+
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
+msgid "Delete task"
+msgstr "Slett oppgave"
+
+#: taiga/permissions/choices.py:60
+msgid "Add issue"
+msgstr "Legg til hendelse"
+
+#: taiga/permissions/choices.py:61
+msgid "Modify issue"
+msgstr "Rediger hendelse"
+
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
+msgid "Delete issue"
+msgstr "Slett hendelse"
+
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Legg til wiki-side"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Endre wiki-side"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
+msgid "Delete wiki page"
+msgstr "Slett wiki-side"
+
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Legg til wiki-lenke"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Endre wiki-lenke"
+
+#: taiga/permissions/choices.py:74
+msgid "Delete wiki link"
+msgstr "Slett wiki-lenke"
+
+#: taiga/permissions/choices.py:78
+msgid "Modify project"
+msgstr "Rediger prosjekt"
+
+#: taiga/permissions/choices.py:79
+msgid "Delete project"
+msgstr "Slett prosjekt"
+
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Legg til medlem"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Fjern medlem"
+
+#: taiga/permissions/choices.py:82
+msgid "Admin project values"
+msgstr "Admin prosjektverdier"
+
+#: taiga/permissions/choices.py:83
+msgid "Admin roles"
+msgstr "Admin roller"
+
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
+msgid "owner"
+msgstr "eier"
+
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
+msgid "Incomplete arguments"
+msgstr "Ufullstendige argumenter"
+
+#: taiga/projects/api.py:154 taiga/users/api.py:242
+msgid "Invalid image format"
+msgstr "Ugyldig bildeformat"
+
+#: taiga/projects/api.py:215
+msgid "Not valid template name"
+msgstr "Ikke et gyldig malnavn"
+
+#: taiga/projects/api.py:218
+msgid "Not valid template description"
+msgstr "Ikke en gyldig malbeskrivelse"
+
+#: taiga/projects/api.py:344
+msgid "Invalid user id"
+msgstr "Ugyldig brukerid"
+
+#: taiga/projects/api.py:350
+msgid "The user doesn't exist"
+msgstr "Brukeren eksisterer ikke"
+
+#: taiga/projects/api.py:354
+msgid "The user must be already a project member"
+msgstr "Brukeren må allerede være et medlem i et prosjekt"
+
+#: taiga/projects/api.py:701
+msgid ""
+"The project must have an owner and at least one of the users must be an "
+"active admin"
+msgstr ""
+"Prosjektet må ha en eier og minst en av brukerne må være en aktiv "
+"administrator"
+
+#: taiga/projects/api.py:735
+msgid "You don't have permisions to see that."
+msgstr "Du har ikke tillatelser til å se det."
+
+#: taiga/projects/attachments/api.py:54
+msgid "Partial updates are not supported"
+msgstr "Delvis oppdateringer støttes ikke"
+
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
+msgid "Project ID not matches between object and project"
+msgstr "Prosjekt ID matcher ikke mellom objekt og prosjekt"
+
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
+msgid "project"
+msgstr "prosjekt"
+
+#: taiga/projects/attachments/models.py:43
+msgid "content type"
+msgstr "innholdstype"
+
+#: taiga/projects/attachments/models.py:45
+msgid "object id"
+msgstr "objektid"
+
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
+msgid "modified date"
+msgstr "redigeringsdato"
+
+#: taiga/projects/attachments/models.py:56
+msgid "attached file"
+msgstr "vedlagt fil"
+
+#: taiga/projects/attachments/models.py:58
+msgid "sha1"
+msgstr "sha1"
+
+#: taiga/projects/attachments/models.py:60
+msgid "is deprecated"
+msgstr "er foreldet"
+
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
+msgid "order"
+msgstr "rekkefølge"
+
+#: taiga/projects/choices.py:23
+msgid "AppearIn"
+msgstr "Vises i"
+
+#: taiga/projects/choices.py:24
+msgid "Jitsi"
+msgstr "Jitsi"
+
+#: taiga/projects/choices.py:25
+msgid "Custom"
+msgstr "Egendefinert"
+
+#: taiga/projects/choices.py:26
+msgid "Talky"
+msgstr "Talky"
+
+#: taiga/projects/choices.py:35
+msgid "This project is blocked due to payment failure"
+msgstr "Dette prosjektet er blokkert på grunn av manglende betaling"
+
+#: taiga/projects/choices.py:36
+msgid "This project is blocked by admin staff"
+msgstr "Dette prosjektet er blokkert av en administrator"
+
+#: taiga/projects/choices.py:37
+msgid "This project is blocked because the owner left"
+msgstr "Dette prosjektet er blokkert fordi eieren stakk"
+
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
+msgid "Text"
+msgstr "Tekst"
+
+#: taiga/projects/custom_attributes/choices.py:29
+msgid "Multi-Line Text"
+msgstr "Tekst med flere linjer"
+
+#: taiga/projects/custom_attributes/choices.py:30
+msgid "Date"
+msgstr "Dato"
+
+#: taiga/projects/custom_attributes/choices.py:31
+msgid "Url"
+msgstr "Url"
+
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
+msgid "type"
+msgstr "type"
+
+#: taiga/projects/custom_attributes/models.py:95
+msgid "values"
+msgstr "verdier"
+
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
+msgid "user story"
+msgstr "brukerhistorie"
+
+#: taiga/projects/custom_attributes/models.py:137
+msgid "task"
+msgstr "oppgave"
+
+#: taiga/projects/custom_attributes/models.py:153
+msgid "issue"
+msgstr "hendelse"
+
+#: taiga/projects/custom_attributes/validators.py:58
+msgid "Already exists one with the same name."
+msgstr "Det finnes allerede en med samme navn."
+
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "status"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "subjekt"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "farge"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "tildelt til"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "Er klientkrav"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "Er team behov"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
+msgid "Comment already deleted"
+msgstr "Kommentaren er allerede slettet"
+
+#: taiga/projects/history/api.py:151
+msgid "Comment not deleted"
+msgstr "Kommentaren er ikke slettet"
+
+#: taiga/projects/history/choices.py:31
+msgid "Change"
+msgstr "Endre"
+
+#: taiga/projects/history/choices.py:32
+msgid "Create"
+msgstr "Opprett"
+
+#: taiga/projects/history/choices.py:33
+msgid "Delete"
+msgstr "Slett"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:23
+#, python-format
+msgid "%(role)s role points"
+msgstr "%(role)s rollepoeng"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:26
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:131
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:157
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:194
+msgid "from"
+msgstr "fra"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:32
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:142
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:163
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:180
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:200
+msgid "to"
+msgstr "til"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:44
+msgid "Added new attachment"
+msgstr "La til nytt vedlegg"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:62
+msgid "Updated attachment"
+msgstr "Oppdatert vedlegg"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:68
+msgid "deprecated"
+msgstr "foreldet"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:70
+msgid "not deprecated"
+msgstr "ikke foreldet"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:86
+msgid "Deleted attachment"
+msgstr "Slettede vedlegg"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:105
+msgid "added"
+msgstr "lagt til"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:110
+msgid "removed"
+msgstr "fjernet"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
+msgid "Unassigned"
+msgstr "Ikke tildelt"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:212
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:87
+msgid "-deleted-"
+msgstr "-slettet-"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:21
+msgid "to:"
+msgstr "til:"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:21
+msgid "from:"
+msgstr "fra:"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:27
+msgid "Added"
+msgstr "Lagt til"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:34
+msgid "Changed"
+msgstr "Endret"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:41
+msgid "Deleted"
+msgstr "Slettet"
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:55
+msgid "added:"
+msgstr "lagt til: "
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:58
+msgid "removed:"
+msgstr "fjernet: "
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80
+msgid "From:"
+msgstr "Fra: "
+
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:64
+#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:81
+msgid "To:"
+msgstr "Til: "
+
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
+msgid "content"
+msgstr "innhold"
+
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
+msgid "blocked note"
+msgstr "blokkert notat"
+
+#: taiga/projects/history/templatetags/functions.py:28
+msgid "sprint"
+msgstr "sprint"
+
+#: taiga/projects/issues/api.py:156
+msgid "You don't have permissions to set this sprint to this issue."
+msgstr "Du har ikke tillatelse til å sette denne sprinten til denne hendelsen."
+
+#: taiga/projects/issues/api.py:160
+msgid "You don't have permissions to set this status to this issue."
+msgstr "Du har ikke tillatelse til å sette denne statusen til denne hendelsen."
+
+#: taiga/projects/issues/api.py:164
+msgid "You don't have permissions to set this severity to this issue."
+msgstr ""
+"Du har ikke tillatelse til å sette denne alvorlighetsgraden til denne "
+"hendelsen."
+
+#: taiga/projects/issues/api.py:168
+msgid "You don't have permissions to set this priority to this issue."
+msgstr ""
+"Du har ikke tillatelse til å sette denne prioriteten til denne hendelsen"
+
+#: taiga/projects/issues/api.py:172
+msgid "You don't have permissions to set this type to this issue."
+msgstr "Du har ikke tillatelse til å sette denne typen til denne hendelsen."
+
+#: taiga/projects/issues/models.py:41
+msgid "severity"
+msgstr "alvorlighetsgrad"
+
+#: taiga/projects/issues/models.py:43
+msgid "priority"
+msgstr "prioritet"
+
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
+msgid "milestone"
+msgstr "milepæl"
+
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
+msgid "finished date"
+msgstr "Sluttdato"
+
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
+msgid "external reference"
+msgstr "ekstern referanse"
+
+#: taiga/projects/likes/models.py:36
+msgid "Like"
+msgstr "Liker"
+
+#: taiga/projects/likes/models.py:37
+msgid "Likes"
+msgstr "Liker"
+
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
+msgid "slug"
+msgstr "slug"
+
+#: taiga/projects/milestones/models.py:46
+msgid "estimated start date"
+msgstr "anslått startdato"
+
+#: taiga/projects/milestones/models.py:47
+msgid "estimated finish date"
+msgstr "anslått sluttdato"
+
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
+msgid "is closed"
+msgstr "er lukket"
+
+#: taiga/projects/milestones/models.py:56
+msgid "disponibility"
+msgstr ""
+
+#: taiga/projects/milestones/models.py:80
+msgid "The estimated start must be previous to the estimated finish."
+msgstr ""
+
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
+
+#: taiga/projects/mixins/blocked.py:31
+msgid "is blocked"
+msgstr "er blokkert"
+
+#: taiga/projects/mixins/ordering.py:49
+#, python-brace-format
+msgid "'{param}' parameter is mandatory"
+msgstr "'{param}' parameter er obligatorisk"
+
+#: taiga/projects/mixins/ordering.py:53
+msgid "'project' parameter is mandatory"
+msgstr "'project' parameter er obligatorisk"
+
+#: taiga/projects/models.py:76
+msgid "email"
+msgstr "epost"
+
+#: taiga/projects/models.py:78
+msgid "create at"
+msgstr "opprett ved"
+
+#: taiga/projects/models.py:80 taiga/users/models.py:154
+msgid "token"
+msgstr "token"
+
+#: taiga/projects/models.py:86
+msgid "invitation extra text"
+msgstr "invitasjon ekstra tekst"
+
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
+msgid "user order"
+msgstr "bruker rekkefølge"
+
+#: taiga/projects/models.py:105
+msgid "The user is already member of the project"
+msgstr "Denne brukeren er allerede medlem av prosjektet"
+
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
+
+#: taiga/projects/models.py:116
+msgid "default US status"
+msgstr "standard brukerhistoriestatuser"
+
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "standardpoeng"
+
+#: taiga/projects/models.py:123
+msgid "default task status"
+msgstr "standard oppgavestatuser"
+
+#: taiga/projects/models.py:126
+msgid "default priority"
+msgstr "standard prioriteter"
+
+#: taiga/projects/models.py:129
+msgid "default severity"
+msgstr "standard alvorlighetsgrad"
+
+#: taiga/projects/models.py:133
+msgid "default issue status"
+msgstr "standard hendelsesstatuser"
+
+#: taiga/projects/models.py:137
+msgid "default issue type"
+msgstr "standard hendelsestyper"
+
+#: taiga/projects/models.py:153
+msgid "logo"
+msgstr "logo"
+
+#: taiga/projects/models.py:163
+msgid "members"
+msgstr "medlemmer"
+
+#: taiga/projects/models.py:166
+msgid "total of milestones"
+msgstr "total av milepæler"
+
+#: taiga/projects/models.py:167
+msgid "total story points"
+msgstr "total historiepoeng"
+
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
+msgid "active backlog panel"
+msgstr "aktivt backlogpanel"
+
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
+msgid "active kanban panel"
+msgstr "aktivt kanbanpanel"
+
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
+msgid "active wiki panel"
+msgstr "aktivt wikipanel"
+
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
+msgid "active issues panel"
+msgstr "aktivt hendelsespanel"
+
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
+msgid "videoconference system"
+msgstr "videokonferansesystem"
+
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
+msgid "videoconference extra data"
+msgstr "videokonferanse ekstra data"
+
+#: taiga/projects/models.py:189
+msgid "creation template"
+msgstr "skapelsesmal"
+
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
+msgid "is private"
+msgstr "er privat"
+
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "anonymes rettigheter"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "brukerrettigheter"
+
+#: taiga/projects/models.py:199
+msgid "is featured"
+msgstr "er omtalt"
+
+#: taiga/projects/models.py:202
+msgid "is looking for people"
+msgstr "er søker etter folk"
+
+#: taiga/projects/models.py:204
+msgid "loking for people note"
+msgstr "søker etter folk notat"
+
+#: taiga/projects/models.py:218
+msgid "project transfer token"
+msgstr "prosjektflyttingstoken"
+
+#: taiga/projects/models.py:222
+msgid "blocked code"
+msgstr "blokkert kode"
+
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
+msgid "updated date time"
+msgstr "oppdatert dato tid"
+
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
+msgid "count"
+msgstr "antall"
+
+#: taiga/projects/models.py:232
+msgid "fans last week"
+msgstr "fans forrige uke"
+
+#: taiga/projects/models.py:235
+msgid "fans last month"
+msgstr "fans forrige måned"
+
+#: taiga/projects/models.py:238
+msgid "fans last year"
+msgstr "fans forrige år"
+
+#: taiga/projects/models.py:244
+msgid "activity last week"
+msgstr "aktivitet forrige uke"
+
+#: taiga/projects/models.py:247
+msgid "activity last month"
+msgstr "aktivitet forrige måned"
+
+#: taiga/projects/models.py:250
+msgid "activity last year"
+msgstr "aktivitet forrige år"
+
+#: taiga/projects/models.py:501
+msgid "modules config"
+msgstr "modulkonfigurasjon"
+
+#: taiga/projects/models.py:553
+msgid "is archived"
+msgstr "er arkivert"
+
+#: taiga/projects/models.py:557
+msgid "work in progress limit"
+msgstr "arbeid som pågår grense"
+
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
+msgid "value"
+msgstr "verdi"
+
+#: taiga/projects/models.py:743
+msgid "default owner's role"
+msgstr "standard eiers rolle"
+
+#: taiga/projects/models.py:761
+msgid "default options"
+msgstr "standardvalg"
+
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
+msgid "us statuses"
+msgstr "bh statuser"
+
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
+msgid "points"
+msgstr "poeng"
+
+#: taiga/projects/models.py:765
+msgid "task statuses"
+msgstr "oppgavestatuser"
+
+#: taiga/projects/models.py:766
+msgid "issue statuses"
+msgstr "hendelsesstatuser"
+
+#: taiga/projects/models.py:767
+msgid "issue types"
+msgstr "hendelsestyper"
+
+#: taiga/projects/models.py:768
+msgid "priorities"
+msgstr "prioriteter"
+
+#: taiga/projects/models.py:769
+msgid "severities"
+msgstr "alvorlighetsgrader"
+
+#: taiga/projects/models.py:770
+msgid "roles"
+msgstr "roller"
+
+#: taiga/projects/notifications/choices.py:30
+msgid "Involved"
+msgstr "Involvert"
+
+#: taiga/projects/notifications/choices.py:31
+msgid "All"
+msgstr "Alle"
+
+#: taiga/projects/notifications/choices.py:32
+msgid "None"
+msgstr "Ingen"
+
+#: taiga/projects/notifications/models.py:64
+msgid "created date time"
+msgstr "opprettet dato tid"
+
+#: taiga/projects/notifications/models.py:68
+msgid "history entries"
+msgstr "loggoppføringer"
+
+#: taiga/projects/notifications/models.py:71
+msgid "notify users"
+msgstr "varsle brukere"
+
+#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
+msgid "Watched"
+msgstr "Fulgt"
+
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
+msgid "Notify exists for specified user and project"
+msgstr ""
+
+#: taiga/projects/notifications/services.py:426
+msgid "Invalid value for notify level"
+msgstr "Ugyldig verdi for varslingsnivå"
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Issue updated
\n"
+" Hello %(user)s,
%(changer)s has updated an issue on %(project)s"
+"p>\n"
+"
Issue #%(ref)s %(subject)s
\n"
+" See issue\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Issue updated\n"
+"Hello %(user)s, %(changer)s has updated an issue on %(project)s\n"
+"See issue #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New issue created
\n"
+" Hello %(user)s,
%(changer)s has created a new issue on "
+"%(project)s
\n"
+" Issue #%(ref)s %(subject)s
\n"
+" See issue\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New issue created\n"
+"Hello %(user)s, %(changer)s has created a new issue on %(project)s\n"
+"See issue #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Issue deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted an issue on %(project)s"
+"p>\n"
+"
Issue #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Issue deleted\n"
+"Hello %(user)s, %(changer)s has deleted an issue on %(project)s\n"
+"Issue #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Sprint updated
\n"
+" Hello %(user)s,
%(changer)s has updated an sprint on "
+"%(project)s
\n"
+" Sprint %(name)s
\n"
+" See sprint\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Sprint updated\n"
+"Hello %(user)s, %(changer)s has updated a sprint on %(project)s\n"
+"See sprint %(name)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the sprint \"%(milestone)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New sprint created
\n"
+" Hello %(user)s,
%(changer)s has created a new sprint on "
+"%(project)s
\n"
+" Sprint %(name)s
\n"
+" See "
+"sprint\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New sprint created\n"
+"Hello %(user)s, %(changer)s has created a new sprint on %(project)s\n"
+"See sprint %(name)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the sprint \"%(milestone)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Sprint deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted an sprint on "
+"%(project)s
\n"
+" Sprint %(name)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Sprint deleted\n"
+"Hello %(user)s, %(changer)s has deleted an sprint on %(project)s\n"
+"Sprint %(name)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Task updated
\n"
+" Hello %(user)s,
%(changer)s has updated a task on %(project)s"
+"p>\n"
+"
Task #%(ref)s %(subject)s
\n"
+" See task\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Task updated\n"
+"Hello %(user)s, %(changer)s has updated a task on %(project)s\n"
+"See task #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New task created
\n"
+" Hello %(user)s,
%(changer)s has created a new task on "
+"%(project)s
\n"
+" Task #%(ref)s %(subject)s
\n"
+" See task\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New task created\n"
+"Hello %(user)s, %(changer)s has created a new task on %(project)s\n"
+"See task #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Task deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a task on %(project)s"
+"p>\n"
+"
Task #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Task deleted\n"
+"Hello %(user)s, %(changer)s has deleted a task on %(project)s\n"
+"Task #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" User Story updated
\n"
+" Hello %(user)s,
%(changer)s has updated a user story on "
+"%(project)s
\n"
+" User Story #%(ref)s %(subject)s
\n"
+" See user story\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"User story updated\n"
+"Hello %(user)s, %(changer)s has updated a user story on %(project)s\n"
+"See user story #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New user story created
\n"
+" Hello %(user)s,
%(changer)s has created a new user story on "
+"%(project)s
\n"
+" User Story #%(ref)s %(subject)s
\n"
+" See user story\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New user story created\n"
+"Hello %(user)s, %(changer)s has created a new user story on %(project)s\n"
+"See user story #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" User Story deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a user story on "
+"%(project)s
\n"
+" User Story #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"User Story deleted\n"
+"Hello %(user)s, %(changer)s has deleted a user story on %(project)s\n"
+"User Story #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the US #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Wiki Page updated
\n"
+" Hello %(user)s,
%(changer)s has updated a wiki page on "
+"%(project)s
\n"
+" Wiki page %(page)s
\n"
+" See Wiki Page\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Wiki Page updated\n"
+"\n"
+"Hello %(user)s, %(changer)s has updated a wiki page on %(project)s\n"
+"\n"
+"See wiki page %(page)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the Wiki Page \"%(page)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New wiki page created
\n"
+" Hello %(user)s,
%(changer)s has created a new wiki page on "
+"%(project)s
\n"
+" Wiki page %(page)s
\n"
+" See "
+"wiki page\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New wiki page created\n"
+"\n"
+"Hello %(user)s, %(changer)s has created a new wiki page on %(project)s\n"
+"\n"
+"See wiki page %(page)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the Wiki Page \"%(page)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Wiki page deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a wiki page on "
+"%(project)s
\n"
+" Wiki page %(page)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Wiki page deleted\n"
+"\n"
+"Hello %(user)s, %(changer)s has deleted a wiki page on %(project)s\n"
+"\n"
+"Wiki page %(page)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/validators.py:48
+msgid "Watchers contains invalid users"
+msgstr "Følgere inneholder ugyldige brukere"
+
+#: taiga/projects/occ/mixins.py:37
+msgid "The version must be an integer"
+msgstr "Versjonen må være et heltall"
+
+#: taiga/projects/occ/mixins.py:60
+msgid "The version parameter is not valid"
+msgstr "Versjonsparameteret er ikke gyldig"
+
+#: taiga/projects/occ/mixins.py:76
+msgid "The version doesn't match with the current one"
+msgstr "Versjonen samsvarer ikke med den nåværende"
+
+#: taiga/projects/occ/mixins.py:95
+msgid "version"
+msgstr "versjon"
+
+#: taiga/projects/permissions.py:44
+msgid ""
+"You can't leave the project if you are the owner or there are no more admins"
+msgstr ""
+"Du kan ikke forlate prosjektet hvis du er eieren eller det ikke er flere "
+"administratorer"
+
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
+msgstr ""
+
+#: taiga/projects/services/members.py:123
+msgid "You have reached your current limit of memberships for private projects"
+msgstr "Du har nådd din nåværende grense for medlemskap for private prosjekter"
+
+#: taiga/projects/services/members.py:127
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+"Du har nådd din nåværende grense for medlemskap for offentlige prosjekter"
+
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
+msgid "You can't have more private projects"
+msgstr "Du kan ikke ha fler private prosjekter"
+
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
+msgid ""
+"This project reaches your current limit of memberships for private projects"
+msgstr ""
+"Dette prosjektet kommer til å nå din nåværende grense for medlemskap for "
+"private prosjekter"
+
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
+msgid "You can't have more public projects"
+msgstr "Du kan ikke ha flere offentlige prosjekter"
+
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+"Dette prosjektet kommer til å nå din nåværende grense for medlemskap for "
+"offentlige prosjekter"
+
+#: taiga/projects/services/stats.py:197
+msgid "Future sprint"
+msgstr "Fremtidig sprint"
+
+#: taiga/projects/services/stats.py:217
+msgid "Project End"
+msgstr "Prosjektslutt"
+
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
+msgid "Token is invalid"
+msgstr "Token er ugyldig"
+
+#: taiga/projects/services/transfer.py:67
+msgid "Token has expired"
+msgstr "Token er utløpt"
+
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "etiketter"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "etiketter farge"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
+msgid "You don't have permissions to set this sprint to this task."
+msgstr "Du har ikke tillatelse til å sette denne sprinten til denne oppgaven."
+
+#: taiga/projects/tasks/api.py:100
+msgid "You don't have permissions to set this user story to this task."
+msgstr ""
+"Du har ikke tillatelse til å sette denne brukerhistorien til denne oppgaven."
+
+#: taiga/projects/tasks/api.py:103
+msgid "You don't have permissions to set this status to this task."
+msgstr "Du har ikke tillatelse til å sette denne statusen til denne oppgaven."
+
+#: taiga/projects/tasks/models.py:58
+msgid "us order"
+msgstr "BH rekkefølge"
+
+#: taiga/projects/tasks/models.py:60
+msgid "taskboard order"
+msgstr "Oppgavetavle rekkefølge"
+
+#: taiga/projects/tasks/models.py:68
+msgid "is iocaine"
+msgstr "Er Iocaine"
+
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
+#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
+msgid "someone"
+msgstr "noen"
+
+#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:11
+#, python-format
+msgid ""
+"\n"
+" You have been invited to Taiga!
\n"
+"Hi! %(full_name)s has sent you an invitation to join project "
+"%(project)s in Taiga. Taiga is a Free, open Source Agile Project "
+"Management Tool.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:17
+#, python-format
+msgid ""
+"\n"
+" And now a few words from the jolly good fellow or sistren
"
+"who thought so kindly as to invite you
\n"
+" %(extra)s
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24
+msgid "Accept your invitation to Taiga"
+msgstr "Godta invitasjonen til Taiga"
+
+#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24
+msgid "Accept your invitation"
+msgstr "Godta din invitasjon"
+
+#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25
+msgid "The Taiga Team"
+msgstr "Taiga Teamet"
+
+#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:6
+#, python-format
+msgid ""
+"\n"
+"You, or someone you know, has invited you to Taiga\n"
+"\n"
+"Hi! %(full_name)s has sent you an invitation to join a project called "
+"%(project)s which is being managed on Taiga, a Free, open Source Agile "
+"Project Management Tool.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:12
+#, python-format
+msgid ""
+"\n"
+"And now a few words from the jolly good fellow or sistren who thought so "
+"kindly as to invite you:\n"
+"\n"
+"%(extra)s\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:18
+msgid "Accept your invitation to Taiga following this link:"
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:20
+msgid ""
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[Taiga] Invitation to join to the project '%(project)s'\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_notification-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" You have been added to a project
\n"
+" Hello %(full_name)s,
you have been added to the project "
+"%(project)s
\n"
+" Go to "
+"project\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_notification-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"You have been added to a project\n"
+"Hello %(full_name)s,you have been added to the project %(project)s\n"
+"\n"
+"See project at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/membership_notification-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[Taiga] Added to the project '%(project)s'\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(old_owner_name)s,
\n"
+" %(new_owner_name)s has accepted your offer and will become the "
+"new project owner for \"%(project_name)s\".
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
+#, python-format
+msgid "%(new_owner_name)s says:
"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
+msgid ""
+"\n"
+" From now on, your new status for this project will be \"admin\"."
+"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 "Fortsett"
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(owner_name)s,\n"
+"%(requester_name)s has requested to become the project owner for "
+"\"%(project_name)s\".\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
+msgid ""
+"\n"
+"Please, go to your project settings if you would like to start the project "
+"transfer from the administration panel.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
+msgid "Go to your project settings:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Project ownership transfer request\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Hi %(receiver_name)s,
\n"
+" %(owner_name)s, the current project owner at \"%(project_name)s\" "
+"would like you to become the new project owner.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
+#, python-format
+msgid ""
+"\n"
+" %(owner_name)s says:
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
+msgid ""
+"\n"
+" Please, click on \"Continue\" to either accept or reject this "
+"proposal.
\n"
+" "
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hi %(receiver_name)s,\n"
+"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
+"you to become the new project owner.\n"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
+#, python-format
+msgid "%(owner_name)s says:"
+msgstr ""
+
+#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
+msgid ""
+"\n"
+"Please, go to the following link to either accept or reject this proposal."
+"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:30
+msgid "Scrum"
+msgstr "Scrum"
+
+#. Translators: Description of scrum project template.
+#: taiga/projects/translations.py:32
+msgid ""
+"The agile product backlog in Scrum is a prioritized features list, "
+"containing short descriptions of all functionality desired in the product. "
+"When applying Scrum, it's not necessary to start a project with a lengthy, "
+"upfront effort to document all requirements. The Scrum product backlog is "
+"then allowed to grow and change as more is learned about the product and its "
+"customers"
+msgstr ""
+
+#. Translators: Name of kanban project template.
+#: taiga/projects/translations.py:35
+msgid "Kanban"
+msgstr "Kanban"
+
+#. Translators: Description of kanban project template.
+#: taiga/projects/translations.py:37
+msgid ""
+"Kanban is a method for managing knowledge work with an emphasis on just-in-"
+"time delivery while not overloading the team members. In this approach, the "
+"process, from definition of a task to its delivery to the customer, is "
+"displayed for participants to see and team members pull work from a queue."
+msgstr ""
+
+#. Translators: User story point value (value = undefined)
+#: taiga/projects/translations.py:45
+msgid "?"
+msgstr "?"
+
+#. Translators: User story point value (value = 0)
+#: taiga/projects/translations.py:47
+msgid "0"
+msgstr "0"
+
+#. Translators: User story point value (value = 0.5)
+#: taiga/projects/translations.py:49
+msgid "1/2"
+msgstr "1/2"
+
+#. Translators: User story point value (value = 1)
+#: taiga/projects/translations.py:51
+msgid "1"
+msgstr "1"
+
+#. Translators: User story point value (value = 2)
+#: taiga/projects/translations.py:53
+msgid "2"
+msgstr "2"
+
+#. Translators: User story point value (value = 3)
+#: taiga/projects/translations.py:55
+msgid "3"
+msgstr "3"
+
+#. Translators: User story point value (value = 5)
+#: taiga/projects/translations.py:57
+msgid "5"
+msgstr "5"
+
+#. Translators: User story point value (value = 8)
+#: taiga/projects/translations.py:59
+msgid "8"
+msgstr "8"
+
+#. Translators: User story point value (value = 10)
+#: taiga/projects/translations.py:61
+msgid "10"
+msgstr "10"
+
+#. Translators: User story point value (value = 13)
+#: taiga/projects/translations.py:63
+msgid "13"
+msgstr "13"
+
+#. Translators: User story point value (value = 20)
+#: taiga/projects/translations.py:65
+msgid "20"
+msgstr "20"
+
+#. Translators: User story point value (value = 40)
+#: taiga/projects/translations.py:67
+msgid "40"
+msgstr "40"
+
+#. Translators: User story status
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
+msgid "New"
+msgstr "Ny"
+
+#. Translators: User story status
+#: taiga/projects/translations.py:78
+msgid "Ready"
+msgstr "Klar"
+
+#. Translators: User story status
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
+msgid "In progress"
+msgstr "Under arbeid"
+
+#. Translators: User story status
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
+msgid "Ready for test"
+msgstr "Klar til test"
+
+#. Translators: User story status
+#: taiga/projects/translations.py:87
+msgid "Done"
+msgstr "Ferdig"
+
+#. Translators: User story status
+#: taiga/projects/translations.py:90
+msgid "Archived"
+msgstr "Arkivert"
+
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
+msgid "Closed"
+msgstr "Lukket"
+
+#. Translators: Task status
+#. Translators: Issue status
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
+msgid "Needs Info"
+msgstr "Trenger info"
+
+#. Translators: Issue status
+#: taiga/projects/translations.py:124
+msgid "Postponed"
+msgstr "Utsatt"
+
+#. Translators: Issue status
+#: taiga/projects/translations.py:126
+msgid "Rejected"
+msgstr "Avslått"
+
+#. Translators: Issue type
+#: taiga/projects/translations.py:134
+msgid "Bug"
+msgstr "Bug"
+
+#. Translators: Issue type
+#: taiga/projects/translations.py:136
+msgid "Question"
+msgstr "Spørsmål"
+
+#. Translators: Issue type
+#: taiga/projects/translations.py:138
+msgid "Enhancement"
+msgstr "Forbedring"
+
+#. Translators: Issue priority
+#: taiga/projects/translations.py:146
+msgid "Low"
+msgstr "Lav"
+
+#. Translators: Issue priority
+#. Translators: Issue severity
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
+msgid "Normal"
+msgstr "Normal"
+
+#. Translators: Issue priority
+#: taiga/projects/translations.py:150
+msgid "High"
+msgstr "Høy"
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:157
+msgid "Wishlist"
+msgstr "Ønskeliste"
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:159
+msgid "Minor"
+msgstr "Liten"
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:163
+msgid "Important"
+msgstr "Viktig"
+
+#. Translators: Issue severity
+#: taiga/projects/translations.py:165
+msgid "Critical"
+msgstr "Kritisk"
+
+#. Translators: User role
+#: taiga/projects/translations.py:172
+msgid "UX"
+msgstr "UX"
+
+#. Translators: User role
+#: taiga/projects/translations.py:174
+msgid "Design"
+msgstr "Design"
+
+#. Translators: User role
+#: taiga/projects/translations.py:176
+msgid "Front"
+msgstr "Front"
+
+#. Translators: User role
+#: taiga/projects/translations.py:178
+msgid "Back"
+msgstr "Back"
+
+#. Translators: User role
+#: taiga/projects/translations.py:180
+msgid "Product Owner"
+msgstr "Produkteier"
+
+#. Translators: User role
+#: taiga/projects/translations.py:182
+msgid "Stakeholder"
+msgstr "Interessent"
+
+#: taiga/projects/userstories/api.py:124
+msgid "You don't have permissions to set this sprint to this user story."
+msgstr ""
+"Du har ikke tillatelse til å sette denne sprinten til denne brukerhistorien."
+
+#: taiga/projects/userstories/api.py:128
+msgid "You don't have permissions to set this status to this user story."
+msgstr ""
+"Du har ikke tillatelse til å sette denne statusen til denne brukerhistorien."
+
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
+#, python-brace-format
+msgid "Generating the user story #{ref} - {subject}"
+msgstr "Genererer brukerhistorien #{ref} - {subject}"
+
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
+msgid "role"
+msgstr "rolle"
+
+#: taiga/projects/userstories/models.py:80
+msgid "backlog order"
+msgstr "backlog rekkefølge"
+
+#: taiga/projects/userstories/models.py:82
+msgid "sprint order"
+msgstr "sprint rekkefølge"
+
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
+msgid "finish date"
+msgstr "Sluttdato"
+
+#: taiga/projects/userstories/models.py:107
+msgid "generated from issue"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:43
+msgid "There's no user story with that id"
+msgstr "Det finnes ingen brukerhistorie med den id'en"
+
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
+msgid "There's no project with that id"
+msgstr "Det finnes ikke noe prosjekt med den id'en"
+
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "E-postadressen er allerede tatt"
+
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Ugyldig rolle for prosjektet"
+
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr "Prosjekteieren skal være admin."
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr "Minst en bruker må være en aktiv administrator for dette prosjektet."
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Standardvalgene"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Brukerhistoriestatuser"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Poeng"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Oppgavestatuser"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Hendelsesstatuser"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Hendelsestyper"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioriteter"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Alvorlighetsgrad"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Roller"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
+msgid "Votes"
+msgstr "Stemmer"
+
+#: taiga/projects/votes/models.py:57
+msgid "Vote"
+msgstr "Stemme"
+
+#: taiga/projects/wiki/api.py:77
+msgid "'content' parameter is mandatory"
+msgstr "'content' parameteren er obligatorisk"
+
+#: taiga/projects/wiki/api.py:80
+msgid "'project_id' parameter is mandatory"
+msgstr "'project_id' parameteren er obligatorisk"
+
+#: taiga/projects/wiki/models.py:42
+msgid "last modifier"
+msgstr "sist endret av"
+
+#: taiga/projects/wiki/models.py:75
+msgid "href"
+msgstr "href"
+
+#: taiga/timeline/signals.py:63
+msgid "Check the history API for the exact diff"
+msgstr "Sjekk historieAPI'et for den eksakte forskjellen"
+
+#: taiga/users/admin.py:39
+msgid "Project Member"
+msgstr "Prosjektmedlem"
+
+#: taiga/users/admin.py:40
+msgid "Project Members"
+msgstr "Prosjektmedlemmer"
+
+#: taiga/users/admin.py:50
+msgid "id"
+msgstr "id"
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr "Prosjekteierskap"
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr "Prosjekteierskap"
+
+#: taiga/users/admin.py:119
+msgid "Personal info"
+msgstr "Personlig informasjon"
+
+#: taiga/users/admin.py:122
+msgid "Permissions"
+msgstr "Tilganger"
+
+#: taiga/users/admin.py:123
+msgid "Restrictions"
+msgstr "Restriksjoner"
+
+#: taiga/users/admin.py:125
+msgid "Important dates"
+msgstr "Viktige datoer"
+
+#: taiga/users/api.py:123
+msgid "Duplicated email"
+msgstr "Duplikat e-post"
+
+#: taiga/users/api.py:125
+msgid "Not valid email"
+msgstr "Ikke gyldig epost"
+
+#: taiga/users/api.py:165
+msgid "Invalid username or email"
+msgstr "Ugyldig brukernavn eller epost"
+
+#: taiga/users/api.py:174
+msgid "Mail sended successful!"
+msgstr "Epost sendt!"
+
+#: taiga/users/api.py:212
+msgid "Current password parameter needed"
+msgstr "Nåværende passord er nødvendig"
+
+#: taiga/users/api.py:215
+msgid "New password parameter needed"
+msgstr "Nytt passord er nødvendig"
+
+#: taiga/users/api.py:218
+msgid "Invalid password length at least 6 charaters needed"
+msgstr "Ugyldig lengde på passord. Minst 6 tegn"
+
+#: taiga/users/api.py:221
+msgid "Invalid current password"
+msgstr "Ugyldig nåværende passord"
+
+#: taiga/users/api.py:268 taiga/users/api.py:274
+msgid ""
+"Invalid, are you sure the token is correct and you didn't use it before?"
+msgstr ""
+"Ugyldig, er du sikker på at token er korrekt og at du ikke har brukt den før?"
+
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
+msgid "Invalid, are you sure the token is correct?"
+msgstr "Ugyldig, er du sikker på at token er korrekt?"
+
+#: taiga/users/models.py:95
+msgid "superuser status"
+msgstr "superbrukerstatus"
+
+#: taiga/users/models.py:96
+msgid ""
+"Designates that this user has all permissions without explicitly assigning "
+"them."
+msgstr ""
+"Angir at denne brukeren har alle tillatelser uten eksplisitt tildele dem."
+
+#: taiga/users/models.py:126
+msgid "username"
+msgstr "brukernavn"
+
+#: taiga/users/models.py:127
+msgid ""
+"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
+msgstr "Påkrevd. 30 tegn eller færre. Bokstaver, tall og /./-/_ tegn"
+
+#: taiga/users/models.py:130
+msgid "Enter a valid username."
+msgstr "Skriv inn et gyldig brukernavn"
+
+#: taiga/users/models.py:133
+msgid "active"
+msgstr "aktiv"
+
+#: taiga/users/models.py:134
+msgid ""
+"Designates whether this user should be treated as active. Unselect this "
+"instead of deleting accounts."
+msgstr ""
+"Betegner om denne brukeren bør behandles som aktiv. Velg bort dette i stedet "
+"for å slette kontoer."
+
+#: taiga/users/models.py:140
+msgid "biography"
+msgstr "biografi"
+
+#: taiga/users/models.py:143
+msgid "photo"
+msgstr "bilde"
+
+#: taiga/users/models.py:144
+msgid "date joined"
+msgstr "dato ble med"
+
+#: taiga/users/models.py:146
+msgid "default language"
+msgstr "standardspråk"
+
+#: taiga/users/models.py:148
+msgid "default theme"
+msgstr "standard tema"
+
+#: taiga/users/models.py:150
+msgid "default timezone"
+msgstr "standard tidssone"
+
+#: taiga/users/models.py:152
+msgid "colorize tags"
+msgstr "fargelegg etiketter"
+
+#: taiga/users/models.py:157
+msgid "email token"
+msgstr "epost token"
+
+#: taiga/users/models.py:159
+msgid "new email address"
+msgstr "ny epostadresse"
+
+#: taiga/users/models.py:166
+msgid "max number of owned private projects"
+msgstr "maks antall eide private prosjekter"
+
+#: taiga/users/models.py:169
+msgid "max number of owned public projects"
+msgstr "maks antall eide offentlige prosjekter"
+
+#: taiga/users/models.py:172
+msgid "max number of memberships for each owned private project"
+msgstr "maks antall medlemskap for hvert eide private prosjekt"
+
+#: taiga/users/models.py:176
+msgid "max number of memberships for each owned public project"
+msgstr "maks antall medlemskap for hvetr eide offentlige prosjekt"
+
+#: taiga/users/models.py:296
+msgid "permissions"
+msgstr "rettigheter"
+
+#: taiga/users/services.py:51 taiga/users/services.py:68
+msgid "Username or password does not matches user."
+msgstr "Brukernavn eller passord passer ikke til brukeren."
+
+#: taiga/users/templates/emails/change_email-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Change your email
\n"
+" Hello %(full_name)s,
please confirm your email
\n"
+" Confirm "
+"email\n"
+" You can ignore this message if you did not request.
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+"\n"
+" Endre din epost
\n"
+" Hallo %(full_name)s,
vennligst bekreft din epost
\n"
+" Bekreft "
+"epost\n"
+" Du kan ignorere denne meldingen dersom du ikke bestilte endringen."
+"p>\n"
+"
Taiga Teamet
\n"
+" "
+
+#: taiga/users/templates/emails/change_email-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hello %(full_name)s, please confirm your email\n"
+"\n"
+"%(url)s\n"
+"\n"
+"You can ignore this message if you did not request.\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+"\n"
+"Hallo %(full_name)s, vennligst bekreft din epost\n"
+"\n"
+"%(url)s\n"
+"\n"
+"Du kan ignorere denne meldingen dersom du ikke bestilte endringen\n"
+"\n"
+"---\n"
+"Taiga Teamet\n"
+
+#: taiga/users/templates/emails/change_email-subject.jinja:1
+msgid "[Taiga] Change email"
+msgstr "[Taiga] Endre epost"
+
+#: taiga/users/templates/emails/password_recovery-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Recover your password
\n"
+" Hello %(full_name)s,
you asked to recover your password
\n"
+" Recover your password\n"
+" You can ignore this message if you did not request.
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/users/templates/emails/password_recovery-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Hello %(full_name)s, you asked to recover your password\n"
+"\n"
+"%(url)s\n"
+"\n"
+"You can ignore this message if you did not request.\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/users/templates/emails/password_recovery-subject.jinja:1
+msgid "[Taiga] Password recovery"
+msgstr ""
+
+#: taiga/users/templates/emails/registered_user-body-html.jinja:6
+msgid ""
+"\n"
+" \n"
+" Thank you for registering in Taiga\n"
+" We hope you enjoy it\n"
+" We built Taiga because we wanted the project management tool "
+"that sits open on our computers all day long, to serve as a continued "
+"reminder of why we love to collaborate, code and design. \n"
+" We built it to be beautiful, elegant, simple to use and fun - "
+"without forsaking flexibility and power. \n"
+" The taiga Team\n"
+" | \n"
+" "
+msgstr ""
+
+#: taiga/users/templates/emails/registered_user-body-html.jinja:23
+#, python-format
+msgid ""
+"\n"
+" You may remove your account from this service clicking "
+"here\n"
+" "
+msgstr ""
+
+#: taiga/users/templates/emails/registered_user-body-text.jinja:1
+msgid ""
+"\n"
+"Thank you for registering in Taiga\n"
+"\n"
+"We hope you enjoy it\n"
+"\n"
+"We built Taiga because we wanted the project management tool that sits open "
+"on our computers all day long, to serve as a continued reminder of why we "
+"love to collaborate, code and design.\n"
+"\n"
+"We built it to be beautiful, elegant, simple to use and fun - without "
+"forsaking flexibility and power.\n"
+"\n"
+"--\n"
+"The taiga Team\n"
+msgstr ""
+
+#: taiga/users/templates/emails/registered_user-body-text.jinja:13
+#, python-format
+msgid ""
+"\n"
+"You may remove your account from this service: %(url)s\n"
+msgstr ""
+"\n"
+"Du kan fjerne din konto fra denne tjenesten: %(url)s\n"
+
+#: taiga/users/templates/emails/registered_user-subject.jinja:1
+msgid "You've been Taigatized!"
+msgstr "Du har blitt Taigatisert!"
+
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "ugyldig"
+
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Ugyldig brukernavn. Prøv med et annet et."
+
+#: taiga/userstorage/api.py:53
+msgid ""
+"Duplicate key value violates unique constraint. Key '{}' already exists."
+msgstr ""
+"Duplicate nøkkelverdi bryter unik begrensning. Nøkkelen \"{}\" finnes "
+"allerede."
+
+#: taiga/userstorage/models.py:32
+msgid "key"
+msgstr "nøkkel"
+
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
+msgid "URL"
+msgstr "URL"
+
+#: taiga/webhooks/models.py:31
+msgid "secret key"
+msgstr "hemmelig nøkkel"
+
+#: taiga/webhooks/models.py:41
+msgid "status code"
+msgstr "statuskode"
+
+#: taiga/webhooks/models.py:42
+msgid "request data"
+msgstr "forespørselsdata"
+
+#: taiga/webhooks/models.py:43
+msgid "request headers"
+msgstr ""
+
+#: taiga/webhooks/models.py:44
+msgid "response data"
+msgstr ""
+
+#: taiga/webhooks/models.py:45
+msgid "response headers"
+msgstr ""
+
+#: taiga/webhooks/models.py:46
+msgid "duration"
+msgstr "varighet"
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index e0d6070d..0a0cd5ad 100644
--- a/taiga/locale/nl/LC_MESSAGES/django.po
+++ b/taiga/locale/nl/LC_MESSAGES/django.po
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -20,159 +20,163 @@ msgstr ""
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Publieke registratie is uitgeschakeld."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "ongeldig registratie type"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "ongeldig login type"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Gebruikersnaame is al in gebruik."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "E-mail adres is al in gebruik."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Token stemt niet overeen met een geldige uitnodiging."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Gebruiker is al geregistreerd."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr ""
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Fout bij het aanmaken van een nieuwe gebruiker."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Ongeldig token"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "ongeldige gebruikersnaam"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Verplicht. 255 tekens of minder. Letters, nummers en /./-/_ tekens'"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Gebruikersnaame is al in gebruik."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "E-mail adres is al in gebruik."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Token stemt niet overeen met een geldige uitnodiging."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Gebruiker is al geregistreerd."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr ""
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Fout bij het aanmaken van een nieuwe gebruiker."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Ongeldig token"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Dit veld is verplicht."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Ongeldige waarde."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' waarde moet True of False zijn."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Geef een geldige 'slug' in bestaande uit letters, nummers, underscores of "
"koppeltekens."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Selecteer een geldige keuze. %(value)s is niet één van de aanwezige "
"keuzemogelijkheden."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Voeg een geldig e-mail adres toe."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr ""
"Datum heeft het verkeerde formaat. Gebruik één van de volgende formaten: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr ""
"Datum en tijd heeft het verkeerde formaat. Gebruik één van de volgende "
"formaten: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr ""
"Tijd heeft een verkeerd formaat. Gebruik één van de volgende formaten: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Geef een geheel getal in."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Zorg ervoor dat deze waarde minder of gelijk is aan %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Zorg ervoor dat deze waarde groter of gelijk is aan %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" waarde dient een float te zijn."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Geef een getal in."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Zorg ervoor dat er niet meer dan %s nummers in totaal zijn."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Zorg ervoor dat er niet meer dan %s plaatsen na de comma zijn."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Zorg ervoor dat er niet meer dan %s nummers voor de comma staan."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr ""
"Er was geen bestand aangegeven. Bekijken het type encoding in het formulier."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Er was geen bestand aangegeven."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Het gegeven bestand is leeg."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -180,13 +184,13 @@ msgstr ""
"Zorg ervoor dat deze bestandsnaam maximaal %(max)d tekens lang is (de naam "
"heeft %(length)d tekens)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Gelieve ofwel een bestand mee te geven ofwel de checkbox aan te tikken, niet "
"beide."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -194,181 +198,178 @@ msgstr ""
"Upload een geldige afbeelding. Het bestand dat je hebt geuploadet was ofwel "
"een afbeelding ofwel een corrupte afbeelding."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Pagina is niet 'last', noch kan het omgezet worden naar een int."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Ongeldige pagina (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Ongeldige definitie van permissie."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Ongeldige pk '%s' - object bestaat niet."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Incorrect type. Pk waarde werd verwacht, maar %s gekregen."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Object met %s=%s bestaat niet."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Ongeldige hyperlink - Geen URL match"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Ongeldige hyperlink - Incorrecte URL match"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Ongeldige hyperlink door configuratiefout"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Ongeldige hyperlink - object bestaat niet."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Incorrect type. Url string werd verwacht, maar %s gekregen."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Ongeldige data"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Geen input gegeven"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Kan geen nieuw item aanmaken, enkel bestaande items mogen bijgewerkt worden."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Verwachtte een lijst van items."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Niet gevonden"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Toestemming geweigerd"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Server applicatie fout"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Verbindingsfout."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Slecht gevormde request."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Incorrecte authenticatie gegevens."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Authenticatie gegevens werden niet gegeven."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Je hebt geen toestemming om deze actie te ondernemen."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Methode '%s' is niet toegestaan."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Kon niet voldoen aan de Accept header van de request"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Niet ondersteund media type '%s' in de request."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Request werd gethrottled."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Verwachtte beschikbaarheid in %d second%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Onverwachte fout"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Niet gevonden."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Methode niet ondersteund voor dit endpoint."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Verkeerde argumenten."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Data validatie fout"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Preconditie fout"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Fout in filter params types."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' moet een integer waarde zijn."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "tags"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -423,7 +424,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -435,26 +436,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" Taiga Support:"
-"strong>\n"
-" %(support_url)s\n"
-"
\n"
-" Contacteer ons:"
-"strong>\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Mailing lijst:"
-"strong>\n"
-" \n"
-" %(mailing_list_url)s\n"
-" \n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -506,103 +487,88 @@ msgstr ""
" Commentaar: %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "We hadden minstens één rol nodig"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Dump file nodig"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Ongeldig dump formaat"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" niet gevonden in dit project"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Ongeldige inhoud. Volgend formaat geldt {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Het bevat ongeldige eigen velden:"
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Naam gedupliceerd voor het project"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "fout bij het importeren van project data"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "fout bij importeren rollen"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "fout bij importeren lidmaatschappen"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "fout bij importeren van project attributenlijst"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "fout bij importeren van standaard projectattributen waarden"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "fout bij importeren eigen attributen"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "fout bij importeren sprints"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "fout bij importeren user stories"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "fout bij importeren taken"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "fout bij importeren issues"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "fout bij importeren user stories"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "fout bij importeren taken"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "fout bij importeren wiki pagina's"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "fout bij importeren wiki links"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "fout bij importeren tags"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "fout bij importeren tijdlijnen"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Fout bij genereren project dump"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -622,15 +588,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Fout bij laden project dump"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -803,77 +769,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Je project dump is geïmporteerd"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" niet gevonden in dit project"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Ongeldige inhoud. Volgend formaat geldt {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Het bevat ongeldige eigen velden:"
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Naam gedupliceerd voor het project"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr ""
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "naam"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr ""
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr ""
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "omschrijving"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr ""
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr ""
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr ""
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr ""
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "volledige naam"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "e-mail adres"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "commentaar"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "aanmaakdatum"
@@ -904,7 +890,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Extra info"
@@ -938,507 +924,577 @@ msgstr ""
"\n"
"[Taiga] Feedback van %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "De payload is geen geldige json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Het project bestaat niet"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Slechte signature"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Het element waarnaar verwezen wordt bestaat niet"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "De status bestaat niet"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Status veranderd door Bitbucket commit"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Ongeldige issue informatie"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Ongeldige issue commentaar informatie"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Ongeldige issue informatie"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Status veranderd door GitHub commit."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Issue aangemaakt via GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Commentaar via GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Het element waarnaar verwezen wordt bestaat niet"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Status veranderd door GitLab commit"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "De status bestaat niet"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Aangemaakt via GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Bekijk project"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Bekijk milestones"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Bekijk user stories"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Bekijk taken"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Bekijk issues"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Bekijk wiki pagina's"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Bekijk wiki links"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Vraag lidmaatschap aan"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Voeg user story toe aan project"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Voeg commentaar toe aan user stories"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Voeg commentaar toe aan taken"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Voeg issues toe"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Voeg commentaar toe aan issues"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Voeg wiki pagina toe"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Wijzig wiki pagina"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Voeg wiki link toe"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Wijzig wiki link"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Voeg milestone toe"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Wijzig milestone"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Verwijder milestone"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Bekijk user story"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Voeg user story toe"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Wijzig user story"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Verwijder user story"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Voeg taak toe"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Wijzig taak"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Verwijder taak"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Voeg issue toe"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Wijzig issue"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Verwijder issue"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Voeg wiki pagina toe"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Wijzig wiki pagina"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Verwijder wiki pagina"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Voeg wiki link toe"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Wijzig wiki link"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Verwijder wiki link"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Wijzig project"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Voeg lid toe"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Verwijder lid"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Verwijder project"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Voeg lid toe"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Verwijder lid"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Admin project waarden"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Admin rollen"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "eigenaar"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Onvolledige argumenten"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Ongeldig afbeelding formaat"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Ongeldige template naam"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Ongeldige template omschrijving"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Je hebt geen toestamming om dat te bekijken."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "Project ID van object is niet gelijk aan die van het project"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "project"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "inhoud type"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "object id"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "gemodifieerde datum"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "bijgevoegd bestand"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "is verouderd"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "volgorde"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr ""
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
-msgid "Text"
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:28
-msgid "Multi-Line Text"
+msgid "Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:29
-msgid "Date"
+msgid "Multi-Line Text"
msgstr ""
#: taiga/projects/custom_attributes/choices.py:30
+msgid "Date"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "type"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "waarden"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "user story"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "taak"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "issue"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Er bestaat er al één met dezelfde naam."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "status"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "onderwerp"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "kleur"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "toegewezen aan"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "is requirement van de klant"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "is requirement van het team"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Commentaar is al verwijderd"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Commentaar niet verwijderd"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Verander"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Creëer"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Verwijder"
@@ -1494,7 +1550,7 @@ msgstr "verwijderd"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Niet toegewezen"
@@ -1541,97 +1597,77 @@ msgstr "Van:"
msgid "To:"
msgstr "Naar:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "inhoud"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "geblokkeerde notitie"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr ""
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Je hebt geen toestemming om deze sprint op deze issue te zetten."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Je hebt geen toestemming om deze status toe te kennen aan dze issue."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"Je hebt geen toestemming om dit ernstniveau toe te kennen aan deze issue."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"Je hebt geen toestemming om deze prioriteit toe te kennen aan deze issue."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Je hebt geen toestemming om dit type toe te kennen aan deze issue."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "status"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "erstniveau"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "prioriteit"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "milestone"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "datum van afwerking"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "onderwerp"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "toegewezen aan"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "externe referentie"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Vind ik leuk"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Personen die dit leuk vinden"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -1643,8 +1679,9 @@ msgstr "geschatte start datum"
msgid "estimated finish date"
msgstr "geschatte datum van afwerking"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "is gesloten"
@@ -1656,290 +1693,384 @@ msgstr "beschikbaarheid"
msgid "The estimated start must be previous to the estimated finish."
msgstr "The geschatte start moet vroeger zijn dan het geschatte einde."
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Er is geen sprint met dat id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "is geblokkeerd"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' parameter is verplicht"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project' parameter is verplicht"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "aangemaakt op"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "uitnodiging extra text"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "gebruiker volgorde"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "The gebruikers is al lid van het project"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "standaard punten"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "standaard US status"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "standaard punten"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "default taak status"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "standaard prioriteit"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "standaard ernstniveau"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "standaard issue status"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "standaard issue type"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "leden"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "totaal van de milestones"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "totaal story points"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "actief backlog paneel"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "actief kanban paneel"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "actief wiki paneel"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "actief issues paneel"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "videoconference systeem"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr ""
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "aanmaak template"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "anonieme toestemmingen"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "gebruikers toestemmingen"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "is privé"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "anonieme toestemmingen"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "gebruikers toestemmingen"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "tag kleuren"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "gewijzigde datum en tijd"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr ""
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "module config"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "is gearchiveerd"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "kleur"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "work in progress limiet"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "waarde"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "standaard rol eigenaar"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "standaard instellingen"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "us statussen"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "punten"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "taak statussen"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "issue statussen"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "issue types"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "prioriteiten"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "ernstniveaus"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "rollen"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr ""
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr ""
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "aanmaak datum en tijd"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "geschiedenis items"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "verwittig gebruikers"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr ""
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Verwittiging bestaat voor gespecifieerde gebruiker en project"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr ""
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2441,159 +2572,179 @@ msgstr ""
"\n"
"[%(project)s] Wiki Pagina verwijderd \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Volgers bevat ongeldige gebruikers"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "De versie moet een integer zijn"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr ""
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "De versie stemt niet overeen met de huidige waarde"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "versie"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "E-mail adres is al in gebruik"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Ongeldige rol voor project"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Standaard opties"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Status van User story"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Punten"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Statussen van taken"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Statussen van Issues"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Types van issue"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Prioriteiten"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Ernstniveaus"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Rollen"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Toekomstige sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Project einde"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Token is ongeldig"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "tags"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "tag kleuren"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr ""
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "us volgorde"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "takenbord volgorde"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "is iocaine"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Er is geen taak met dat id"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -2947,12 +3098,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -2970,12 +3121,12 @@ msgstr ""
"gebruikers"
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -2988,303 +3139,388 @@ msgstr ""
"definitie tot taak tot het afleveren naar de klant."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Nieuw"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Klaar"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "Lopende"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Klaar om te testen"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Afgewerkt"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Gearchiveerd"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Gesloten"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Info nodig"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Verzet naar later"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Geweigerd"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Bug"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Vraag"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Verbetering"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Laag"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normaal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Hoog"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Wensenlijst"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Mineur"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Belangrijk"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Kritiek"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Product Owner"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rol"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "backlog volgorde"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "sprint volgorde"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "afwerkdatum"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "is requirement van de klant"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "is requirement van het team"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "gegenereerd van issue"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Er is geen user story met dat id"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Er is geen project met dat is"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Er is geen user story status met dat id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "E-mail adres is al in gebruik"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Er is geen taak status met dat id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Ongeldige rol voor project"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Standaard opties"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Status van User story"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Punten"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Statussen van taken"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Statussen van Issues"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Types van issue"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioriteiten"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Ernstniveaus"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Rollen"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Stemmen"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Stem"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'inhoud' parameter is verplicht"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parameter is verplicht"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "gebruiker met laatste wijziging"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3312,52 +3548,52 @@ msgstr ""
msgid "Important dates"
msgstr "Belangrijke data"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Gedupliceerde e-mail"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Ongeldige e-mail"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Ongeldige gebruikersnaam of e-mail"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Mail met succes verzonden!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Huidig wachtwoord parameter vereist"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Nieuw wachtwoord parameter vereist"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Ongeldige lengte van wachtwoord, minstens 6 tekens vereist"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Ongeldig huidig wachtwoord"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr "Ongeldig, weet je zeker dat het token correct en ongebruikt is?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Ongeldig, weet je zeker dat het token correct is?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "superuser status"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3365,24 +3601,24 @@ msgstr ""
"Beduidt dat deze gebruik alle toestemmingen heeft zonder deze expliciet toe "
"te wijzen."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "gebruikersnaam"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Vereist. 30 of minder karakters. Letters, nummers en /./-/_ karakters"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Geef een geldige gebruikersnaam in"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "actief"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3390,71 +3626,63 @@ msgstr ""
"Beduidt of deze gebruiker als actief moet behandeld worden. Deselecteer dit "
"i.p.v. accounts te verwijderen."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografie"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "toetrededatum"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "standaard taal"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr ""
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "standaard tijdzone"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "kleur tags"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "e-mail token"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "nieuw e-mail adres"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "toestemmingen"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "ongeldig"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Ongeldige gebruikersnaam. Probeer met een andere."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker."
@@ -3577,48 +3805,52 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Je bent getaiganiseerd!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Er is geen rol met dat id"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "ongeldig"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Ongeldige gebruikersnaam. Probeer met een andere."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
"Gedupliceerde key value overtreed unieke constraint. Key '{}' bestaat al."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "key"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "geheime sleutel"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "status code"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "request data"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "request headers"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "response data"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "response headers"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duur"
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index f7ce189e..b40c46b2 100644
--- a/taiga/locale/pl/LC_MESSAGES/django.po
+++ b/taiga/locale/pl/LC_MESSAGES/django.po
@@ -10,8 +10,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -22,153 +22,157 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Publiczna rejestracja jest wyłączona"
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "Nieprawidłowy typ rejestracji"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "Nieprawidłowy typ logowania"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Nazwa użytkownika jest już używana."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Ten adres email jest już w użyciu."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Token nie zgadza się z żadnym zaproszeniem"
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Użytkownik już zarejestrowany"
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr ""
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Błąd przy tworzeniu użytkownika."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Nieprawidłowy token"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "Nieprawidłowa nazwa użytkownika"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Wymagane. Maksymalnie 255 znaków. Litery, cyfry oraz /./-/_ "
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Nazwa użytkownika jest już używana."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Ten adres email jest już w użyciu."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Token nie zgadza się z żadnym zaproszeniem"
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Użytkownik już zarejestrowany"
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr ""
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Błąd przy tworzeniu użytkownika."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Nieprawidłowy token"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "To pole jest wymagane."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Nieprawidłowa wartość."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' wartość musi przyjąć True albo False,"
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Podaj prawidłowy 'slug' zawierający litery, cyfry, podkreślenia lub myślniki."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Dokonał właściwego wyboru. Wartość %(value)s nie jest jedną z dostępnych "
"opcji."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Podaj właściwy adres email."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "Zły format. Użyj jednego z tych formatów: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "Zły format. Użyj jednego z tych formatów: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "Zły format. Użyj jednego z tych formatów: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Wpisz cały numer"
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Upewnij się, że wartość jest mniejsza lub równa od %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Upewnij się, że wartość jest większa lub równa od %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" wartość musi być zmiennoprzecinkowa."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Wpisz numer."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Upewnij się że nie podałeś więcej niż %s znaków."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Upewnij się, że nie ma więcej niż %s miejsc po przecinku."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Upewnij się, że nie ma więcej niż %s cyfr przed przecinkiem."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Plik nie został wysłany. Sprawdź kodowanie znaków w formularzu."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Plik nie został wysłany."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Wysłany plik jest pusty."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -176,11 +180,11 @@ msgstr ""
"Upewnij się, że nazwa pliku ma maksymalnie %(max)d znaków.(Ilość znaków to: "
"%(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "Proszę wybrać jedną z opcji, nie obie."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -188,182 +192,179 @@ msgstr ""
"Prześlij właściwy obraz. Plik który próbujesz przesłać nie jest obrazem lub "
"jest uszkodzony."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Strona nie jest ostatnią i nie może zostać zmieniona na int."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Niewłaściwa strona (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Nieprawidłowa definicja uprawnień."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Nieprawidłowa wartość klucza '%s' -Obiekt nie istniej."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Niepoprawny typ. Oczekiwana wartość, otrzymana %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Obiekt z %s=%s nie istnieje."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Nieprawidłowy odnośnik - brak pasującego URL"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Nieprawidłowy odnośnik - źle dopasowany URL"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Nieprawidłowy odnośnik z powodu błędu konfiguracji"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Nieprawidłowy odnośnik - obiekt nie istnieje."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Niepoprawny typ. Oczekiwany url, otrzymany %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Nieprawidłowa dana"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Nic nie wpisano"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Nie można utworzyć nowego obiektu, tylko istniejące obiekty mogą być "
"aktualizowane."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Oczekiwana lista elementów."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Nie znaleziono"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Dostęp zabroniony"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Błąd aplikacji serwera"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Błąd połączenia."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Błędne żądanie."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Nieprawidłowe dane uwierzytelniające."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Nie podano danych uwierzytelniających."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Nie masz uprawnień do wykonania tej czynności."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Metoda %s nie dozwolona."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Nie udało się spełnić żądania Accept Header"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Niewspierany typ pliku '%s' w żądaniu."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Żądanie zostało zduszone."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Oczekiwana dostępność w ciągu %d sekund%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Nieoczekiwany błąd"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Nie odnaleziono."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Metoda nie wspierana dla tej końcówki."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Złe argumenty."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Błąd walidacji dancyh"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Błąd integralności dla błędnych lub nieprawidłowych argumentów"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Błąd warunków wstępnych"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Błąd w parametrach typów filtrów."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' musi być wartością typu int."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "tagi"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -418,7 +419,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -430,26 +431,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" Pomoc Taiga:"
-"strong>\n"
-" %(support_url)s\n"
-"
\n"
-" Skontaktuj się z "
-"nami:\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Lista mailingowa:"
-"\n"
-" \n"
-" %(mailing_list_url)s\n"
-" \n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -507,103 +488,88 @@ msgstr ""
" Komentarz: %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Potrzeba conajmiej jednej roli"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Wymagany plik zrzutu"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Nieprawidłowy format zrzutu"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" nie odnaleziono w projekcie"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Niewłaściwa zawartość. Musi to być {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Zawiera niewłaściwe pola niestandardowe."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Nazwa projektu zduplikowana"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "błąd w trakcie importu danych projektu"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "błąd w trakcie importu ról"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "błąd w trakcie importu członkostw"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "błąd w trakcie importu atrybutów projektu"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "błąd w trakcie importu domyślnych atrybutów projektu"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "błąd w trakcie importu niestandardowych atrybutów"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "błąd w trakcie importu sprintów"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "błąd w trakcie importu historyjek użytkownika"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "błąd w trakcie importu zadań"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "błąd w trakcie importu zgłoszeń"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "błąd w trakcie importu historyjek użytkownika"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "błąd w trakcie importu zadań"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "błąd w trakcie importu stron Wiki"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "błąd w trakcie importu linków Wiki"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "błąd w trakcie importu tagów"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "błąd w trakcie importu osi czasu"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Błąd w trakcie generowania zrzutu projektu"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -623,15 +589,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Błąd w trakcie wczytywania zrzutu projektu"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -871,77 +837,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Twój zrzut projektu został prawidłowo zaimportowany"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" nie odnaleziono w projekcie"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Niewłaściwa zawartość. Musi to być {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Zawiera niewłaściwe pola niestandardowe."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Nazwa projektu zduplikowana"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr ""
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "nazwa"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr ""
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "opis"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Następny url"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr ""
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "użytkownik"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "aplikacja"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "Imię i Nazwisko"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "adres e-mail"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "komentarz"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "data utworzenia"
@@ -972,7 +958,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Dodatkowe info"
@@ -1006,547 +992,577 @@ msgstr ""
"\n"
"[Taiga] Informacje od %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "Źródło nie jest prawidłowym plikiem json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Projekt nie istnieje"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Błędna sygnatura"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Element referencyjny nie istnieje"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Status nie istnieje"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Status zmieniony przez commit z BitBucket"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Nieprawidłowa informacja o zgłoszeniu"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-"Zgłoszenie utworzone przez [@{bitbucket_user_name}]({bitbucket_user_url} "
-"\"Zobacz profil użytkownika @{bitbucket_user_name}'s \") na BitBucket.\n"
-"Źródłowe zgłoszenie z BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Idź do 'bb#{number} - {subject}'\"):\n"
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
"\n"
-"{description}"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Zgłoszenie utworzone przez BitBucket."
-
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Nieprawidłowa informacja o komentarzu do zgłoszenia"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-"Skomentowane przez [@{bitbucket_user_name}]({bitbucket_user_url} \"Zobacz "
-"profil użytkownika @{bitbucket_user_name}'s\") na BitBucket.\n"
-"Źródłowe zgłoszenie z BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Idź do 'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Nieprawidłowa informacja o zgłoszeniu"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Komentarz z BitBucket:\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Status zmieniony przez [@{github_user_name}]({github_user_url} \"Zobacz "
-"profil użytkownika @{github_user_name}'s \") z commitu na GitHub "
-"[{commit_id}]({commit_url} \"Zobacz commit'{commit_id} - "
-"{commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Status zmieniony przez commit z GitHub"
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"Zgłoszenie utworzone przez [@{github_user_name}]({github_user_url} \"Zobacz "
-"profil użytkownika @{github_user_name}'s \") na GitHub.\n"
-"Źródłowe zgłoszenie z GitHub: [gh#{number} - {subject}]({github_url} \"Idź "
-"do 'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Zgłoszenie utworzone przez GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-"Skomentowane przez [@{github_user_name}]({github_user_url} \"Zobacz profil "
-"użytkownika @{github_user_name}'s GitHub profile\") na GitHub.\n"
-"Źródłowe zgłoszenie z GitHub: [gh#{number} - {subject}]({github_url} \"Idź "
-"do 'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Komentarz z GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Element referencyjny nie istnieje"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Status zmieniony przez commit z GitLab"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Status nie istnieje"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Utworzone przez GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Skomentowane przez [@{gitlab_user_name}]({gitlab_user_url} \"Zobacz profil "
-"użytkownika @{gitlab_user_name}'s \") na GitLab.\n"
-"Źródłowe zgłoszenie z: [gl#{number} - {subject}]({gitlab_url} \"Idź do "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Komentarz z GitLab:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Zobacz projekt"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Zobacz kamienie milowe"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Zobacz historyjki użytkownika"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Zobacz zadania"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Zobacz zgłoszenia"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Zobacz strony Wiki"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Zobacz linki Wiki"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Poproś o członkowstwo"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Dodaj historyjkę użytkownika do projektu"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Dodaj komentarze do historyjek użytkownika"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Dodaj komentarze do zadań"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Dodaj zgłoszenia"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Dodaj komentarze do zgłoszeń"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Dodaj strony Wiki"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Modyfikuj stronę Wiki"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Dodaj link do Wiki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modyfikuj link do Wiki"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Dodaj kamień milowy"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modyfikuj Kamień milowy"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Usuń kamień milowy"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Zobacz historyjkę użytkownika"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Dodaj historyjkę użytkownika"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Modyfikuj historyjkę użytkownika"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Usuń historyjkę użytkownika"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Dodaj zadanie"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modyfikuj zadanie"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Usuń zadanie"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Dodaj zgłoszenie"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Modyfikuj zgłoszenie"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Usuń zgłoszenie"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Dodaj strony Wiki"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Modyfikuj stronę Wiki"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Usuń stronę Wiki"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Dodaj link do Wiki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modyfikuj link do Wiki"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Usuń link Wiki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Modyfikuj projekt"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Dodaj członka zespołu"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Usuń członka zespołu"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Usuń projekt"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Dodaj członka zespołu"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Usuń członka zespołu"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Administruj wartościami projektu"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Administruj rolami"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "właściciel"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Pola niekompletne"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Niepoprawny format obrazka"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Nieprawidłowa nazwa szablonu"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Nieprawidłowy opis szablonu"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Nie masz uprawnień by to zobaczyć."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr ""
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "ID nie pasuje pomiędzy obiektem a projektem"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "projekt"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "typ zawartości"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "id obiektu"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "data modyfikacji"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "załączony plik"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr ""
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "jest przestarzałe"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "kolejność"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Niestandardowy"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Tekst"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Teks wielowierszowy"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "typ"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "wartości"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "historyjka użytkownika"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "zadanie"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "zgłoszenie"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Już istnieje jeden z taką nazwą."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "status"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "temat"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "kolor"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "przypisane do"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "wymaganie klienta"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "wymaganie zespołu"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Komentarz został już usunięty"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Komentarz nie został usunięty"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Zmień"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Utwórz"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Usuń"
@@ -1602,7 +1618,7 @@ msgstr "usuniete"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Nieprzypisane"
@@ -1649,95 +1665,75 @@ msgstr "Od:"
msgid "To:"
msgstr "Do:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "zawartość"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "zaglokowana notatka"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Nie masz uprawnień do połączenia tego zgłoszenia ze sprintem."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Nie masz uprawnień do ustawienia statusu dla tego zgłoszenia."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "Nie masz uprawnień do ustawienia ważności dla tego zgłoszenia."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "Nie masz uprawnień do ustawienia priorytetu dla tego zgłoszenia."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Nie masz uprawnień do ustawienia typu dla tego zgłoszenia."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "status"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "ważność"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "priorytet"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "kamień milowy"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "data zakończenia"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "temat"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "przypisane do"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "źródło zgłoszenia"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr ""
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr ""
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -1749,8 +1745,9 @@ msgstr "szacowana data rozpoczecia"
msgid "estimated finish date"
msgstr "szacowana data zakończenia"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "jest zamknięte"
@@ -1762,290 +1759,384 @@ msgstr "dostępność"
msgid "The estimated start must be previous to the estimated finish."
msgstr "Szacowana data rozpoczęcia musi być wcześniejsza niż data zakończenia."
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Nie ma sprintu o takim ID"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "jest zablokowane"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' parametr jest obowiązkowy"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project' parametr jest obowiązkowy"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "e-mail"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "utwórz na"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "dodatkowy tekst w zaproszeniu"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "kolejność użytkowników"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "Użytkownik już jest członkiem tego projektu"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "domyślne punkty"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "domyślny status dla HU"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "domyślne punkty"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "domyślny status dla zadania"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "domyślny priorytet"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "domyślna ważność"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "domyślny status dla zgłoszenia"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "domyślny typ dla zgłoszenia"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "członkowie"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "wszystkich kamieni milowych"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "wszystkich punktów "
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "aktywny panel backlog"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "aktywny panel Kanban"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "aktywny panel Wiki"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "aktywny panel zgłoszeń "
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "system wideokonferencji"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "dodatkowe dane dla wideokonferencji"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "szablon "
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "uprawnienia anonimowych"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "uprawnienia użytkownika"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "jest prywatna"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "uprawnienia anonimowych"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "uprawnienia użytkownika"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "kolory tagów"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "data aktualizacji"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "ilość"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "konfiguracja modułów"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "zarchiwizowane"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "kolor"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "limit postępu prac"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "wartość"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "domyśla rola właściciela"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "domyślne opcje"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "statusy HU"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "pinkty"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "statusy zadań"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "statusy zgłoszeń"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "typy zgłoszeń"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "priorytety"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "ważność"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "role"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr ""
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr ""
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr ""
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "data utworzenia"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "wpisy historii"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "powiadom użytkowników"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Obserwowane"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Powiadomienie istnieje dla określonego użytkownika i projektu"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Nieprawidłowa wartość dla poziomu notyfikacji"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2778,160 +2869,180 @@ msgstr ""
"\n"
"[%(project)s] Usunął stronę Wiki \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Obserwatorzy zawierają niepoprawnych użytkowników"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "Wersja musi być integerem ;)"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "Parametr wersji jest nieprawidłowy"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Podana wersja nie zgadza się z aktualną."
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "wersja"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Tena adres e-mail jest już w użyciu"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Nieprawidłowa rola w projekcie"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Domyślne opcje"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Statusy historyjek użytkownika"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Punkty"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Statusy zadań"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Statusy zgłoszeń"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Typu zgłoszeń"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Priorytety"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Ważność"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Role"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Przyszły sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Zakończenie projektu"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Nieprawidłowy token."
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "tagi"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "kolory tagów"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "Nie masz uprawnień do ustawiania sprintu dla tego zadania."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Nie masz uprawnień do ustawiania historyjki użytkownika dla tego zadania"
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania"
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "kolejność HU"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "Kolejność tablicy zadań"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "Iokaina"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Nie ma zadania z takim ID"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3313,12 +3424,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3336,12 +3447,12 @@ msgstr ""
"klienta."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3353,305 +3464,390 @@ msgstr ""
"wyświetlane dla klienta a członkowie zespołu wyciągają je z kolejki."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Nowe"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Gotowe"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "W toku"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Gotowe do testów"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Gotowe!"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Zarchiwizowane"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Zamknięte"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Potrzebne informacje"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Odroczone"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Odrzucone"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Błąd"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Pytanie"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Ulepszenie"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Niski"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normalny"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Wysoki"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Życzenie"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Pomniejsze"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Istotne"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Krytyczne"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Właściciel produktu"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Interesariusz"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Nie masz uprawnień do ustawiania sprintu dla tej historyjki użytkownika."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Nie masz uprawnień do ustawiania statusu do tej historyjki użytkownika."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rola"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "Kolejność backlogu"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "kolejność sprintu"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "data zakończenia"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "wymaganie klienta"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "wymaganie zespołu"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "wygenerowane ze zgłoszenia"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Nie ma historyjki użytkownika z takim ID"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Nie ma projektu z takim ID"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Nie ma statusu historyjki użytkownika z takim ID"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Tena adres e-mail jest już w użyciu"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Nie ma statusu zadania z takim ID"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Nieprawidłowa rola w projekcie"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Domyślne opcje"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Statusy historyjek użytkownika"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Punkty"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Statusy zadań"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Statusy zgłoszeń"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Typu zgłoszeń"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Priorytety"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Ważność"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Role"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Głosy"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Głos"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "Parametr 'zawartość' jest wymagany"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "Parametr 'id_projektu' jest wymagany"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "ostatnio zmodyfikowane przez"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Dla pełengo diffa sprawdź API historii"
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3679,56 +3875,56 @@ msgstr ""
msgid "Important dates"
msgstr "Ważne daty"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Zduplikowany adres e-mail"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Niepoprawny adres e-mail"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Nieprawidłowa nazwa użytkownika lub adrs e-mail"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "E-mail wysłany poprawnie!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Należy podać bieżące hasło"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Należy podać nowe hasło"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr ""
"Nieprawidłowa długość hasła - wymagane jest co najmniej 6 "
"strong>znaków"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Podałeś nieprawidłowe bieżące hasło"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Niepoprawne, jesteś pewien, że token jest poprawny i nie używałeś go "
"wcześniej? "
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Niepoprawne, jesteś pewien, że token jest poprawny?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "status SUPERUSER"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3736,24 +3932,24 @@ msgstr ""
"Oznacza, że ten użytkownik posiada wszystkie uprawnienia bez konieczności "
"ich przydzielania."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "nazwa użytkownika"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Wymagane. 30 znaków. Liter, cyfr i znaków /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Wprowadź poprawną nazwę użytkownika"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "aktywny"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3761,71 +3957,63 @@ msgstr ""
"Oznacza, że ten użytkownik ma być traktowany jako aktywny. Możesz to "
"odznaczyć zamiast usuwać konto."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "zdjęcie"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "data dołączenia"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "domyślny język Taiga"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "domyślny szablon Taiga"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "domyśla strefa czasowa"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "kolory tagów"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "tokem e-mail"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "nowy adres e-mail"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "uprawnienia"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "Niepoprawne"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Nazwa użytkownika lub hasło są nieprawidłowe"
@@ -4017,47 +4205,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Zostałeś zaTaigowany"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Nie istnieje rola z takim ID"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "Niepoprawne"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr "Duplikowanie wartości klucza. Klucz '{}' już istnieje."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "klucz"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "sekretny klucz"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "kod statusu"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "data żądania"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "nagłówki żądań"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "dane odpowiedzi"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "nagłówki odpowiedzi"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "czas trwania"
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 2e440979..6014decf 100644
--- a/taiga/locale/pt_BR/LC_MESSAGES/django.po
+++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po
@@ -3,6 +3,7 @@
# This file is distributed under the same license as the taiga-back package.
#
# Translators:
+# Antônio "acdc" Jr. , 2016
# Cléber Zavadniak , 2015
# Thiago , 2015
# Daniel Dias , 2015
@@ -10,17 +11,19 @@
# Hevertton Barbosa , 2015
# Kemel Zaidan , 2015
# Lennon Jesus , 2016
+# Mairieli Wessel , 2016
# Marlon Carvalho , 2015
# pedromvm , 2015
# Renato Prado , 2015
+# Thiago Almeida , 2016
# Thiago , 2015
# Walker de Alencar , 2015
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/pt_BR/)\n"
@@ -30,152 +33,156 @@ msgstr ""
"Language: pt_BR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Registro público está desabilitado. "
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "tipo de registro inválido"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "tipo de login inválido"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Nome de usuário já está em uso."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Este e-mail já está em uso."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Esse token não bate com nenhum convite."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Este usuário já está registrado."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "O usuário já é membro do projeto."
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Erro ao criar um novo usuário."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Token inválido"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "nome de usuário inválido"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Obrigatório. No máximo 255 caracteres. Letras, números e /./-/_ ."
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Nome de usuário já está em uso."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Este e-mail já está em uso."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Esse token não bate com nenhum convite."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Este usuário já está registrado."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "O usuário já é membro do projeto."
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Erro ao criar um novo usuário."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Token inválido"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Este campo é obrigatório."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Valor inválido."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "O valor de '%s' deve ser ou True ou False."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Entre uma 'slug' válida, consistindo de letras, números, underscores ou "
"hífens."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "Escolha uma alternativa válida. %(value)s não está disponível."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Preencha com um e-mail válido."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "A data está no formato errado. Use um desses no lugar: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "Formato da data e hora errado. Use um destes: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "Hora com formato errado. Use um destes: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Insira um número inteiro."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Garanta que o valor é menor ou igual a %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Garanta que o valor é maior ou igual a %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "O valor de \"%s\" deve ser decimal (float)."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Insira um número."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Garanta que não há mais que %s dígitos no total."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Garanta que não há mais que %s casas decimais."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Garanta que não há mais que %s dígitos antes do ponto decimal."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Nenhum arquivo enviado. Verifique o tipo de codificação no formulário."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Nenhum arquivo enviado."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "O arquivo enviado está vazio."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -183,11 +190,11 @@ msgstr ""
"Garanta que o nome do arquivo tem no máximo %(max)d caracteres (no momento "
"tem %(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "Envie um arquivo ou marque o checkbox \"vazio\", não ambos."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -195,182 +202,179 @@ msgstr ""
"Envie uma imagem válida. O arquivo que você mandou ou não era uma imagem ou "
"está corrompido."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
-msgstr ""
+msgstr "Elemento bloqeado"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Página não é \"última\", nem pode ser convertída para um inteiro."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Página inválida (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Definição de permissão inválida."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Chave primária '%s' inválida - objeto não existe."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Tipo incorreto. Esperado valor de chave primária, recebido %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Objeto com %s=%s não existe."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Hyperlink inválido - Nenhuma URL corresponde"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Hyperlink inválido - Corresponde a URL incorreta"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Hyperlink inválido devido a erro de configuração"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Hyperlink inválido - objeto não existe."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Tipo incorreto. Esperada string de url, recebido %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Dados inválidos"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Nenhuma entrada providenciada"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Não é possível criar um novo item, somente itens já existentes podem ser "
"atualizados."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Esperada uma lista de itens."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Não encontrado"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Permissão negada"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Erro no servidor da aplicação"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Erro na conexão."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Requisição mal-formada"
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Credenciais de autenticação incorretas."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Credenciais de autenticação não informadas."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Você não possui permissão para executar esta ação."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Método '%s' não é permitido"
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Não foi possível satisfazer o cabeçalho Accept da requisição"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Tipo de mídia '%s' não suportado na requisição."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Requisição foi sujeita a limites."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Esperado disponível em %d segundo%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Erro inesperado"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Não encontrado."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Método não suportado por esse endpoint."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Argumentos errados."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Erro de validação dos dados"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Erro de Integridade para argumentos inválidos ou errados"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Erro de pré-condição"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Erro nos tipos de parâmetros do filtro."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'projeto' deve ser um valor inteiro."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "tags"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -425,7 +429,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -437,27 +441,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" Suporte Taiga:"
-"strong>\n"
-" %(support_url)s\n"
-"
\n"
-" Entre em contato:"
-"\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Lista de e-mail:"
-"\n"
-" \n"
-" %(mailing_list_url)s\n"
-" \n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -515,103 +498,88 @@ msgstr ""
" Comentário: %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Nós precisamos de pelo menos uma função"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Necessário de arquivo de restauração"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Formato de aquivo de restauração inválido"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" não encontrado nesse projeto"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "conteúdo inválido. Deve ser {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Contém campos personalizados inválidos"
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Nome duplicado para o projeto"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "erro ao importar informações de projeto"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "erro importando funcões"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "erro importando filiações"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "erro importando lista de atributos do projeto"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "erro importando valores de atributos do projeto padrão"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "erro importando atributos personalizados"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "erro importando sprints"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "erro importando user stories"
+#: taiga/export_import/services/store.py:782
+msgid "error importing issues"
+msgstr "erro importando problemas"
-#: taiga/export_import/services/store.py:687
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "erro importando histórias de usuário"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
msgid "error importing tasks"
msgstr "erro importando tarefas"
-#: taiga/export_import/services/store.py:691
-msgid "error importing issues"
-msgstr "erro importando casos"
-
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "erro importando páginas wiki"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "erro importando wiki links"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "erro importando tags"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "erro importando linha do tempo"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
-msgstr ""
+msgstr "erro inesperado ao importar projeto"
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Erro gerando arquivo de restauração do projeto"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -630,18 +598,40 @@ msgid ""
"TRACE ERROR:\n"
"------------"
msgstr ""
+"\n"
+"\n"
+"\n"
+"Erro ao carregar arquivo de restauração por {user_full_name} <{user_email}>:"
+"\"\n"
+"\n"
+"\n"
+"\n"
+"\n"
+"MOTIVO:\n"
+"\n"
+"-------\n"
+"\n"
+"{reason}\n"
+"\n"
+"\n"
+"DETALHES:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"MAIS INFORMAÇÕES DO ERRO:\n"
+"------------"
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Erro carregando arquivo de restauração do projeto"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
-msgstr ""
+msgstr "Erro ao carregar arquivo de restauração do projeto"
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
-msgstr ""
+msgstr "-- sem informações detalhadas --"
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
@@ -878,77 +868,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] A restauração do seu projeto foi importada"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" não encontrado nesse projeto"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "conteúdo inválido. Deve ser {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Contém campos personalizados inválidos"
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Nome duplicado para o projeto"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Autenticação necessária"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "Nome"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "Ícone da url"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "descrição"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Próxima url"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "chave secreta para cifrar os tokens da aplicação"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "usuário"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "aplicação"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "nome completo"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "endereço de e-mail"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "comentário"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "data de criação"
@@ -979,7 +989,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informação extra"
@@ -1013,546 +1023,579 @@ msgstr ""
"\n"
"[Taiga] Resposta de %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "A carga não é um json válido"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "O projeto não existe"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Assinatura Ruim"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
+#: taiga/hooks/event_hooks.py:66
+#, python-brace-format
+msgid ""
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
+"\n"
+"\"{comment_message}\""
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:84
+msgid "Invalid issue comment information"
+msgstr "Informação de comentário de problema inválida"
+
+#: taiga/hooks/event_hooks.py:103
+#, python-brace-format
+msgid ""
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Informação de problema inválida"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
+#, python-brace-format
+msgid ""
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:161
+#, python-brace-format
+msgid ""
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:179
+#, python-brace-format
+msgid ""
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:184
+#, python-brace-format
+msgid ""
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:206
msgid "The referenced element doesn't exist"
msgstr "O elemento referenciado não existe"
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
+#: taiga/hooks/event_hooks.py:222
msgid "The status doesn't exist"
msgstr "O estatus não existe"
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Status alterado em Bitbucket commit"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Informação de caso inválida"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
-#, python-brace-format
-msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-msgstr ""
-"Caso criado por [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Caso criado pelo Bitbucket."
-
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
-msgid "Invalid issue comment information"
-msgstr "Informação de comentário de caso inválido"
-
-#: taiga/hooks/bitbucket/event_hooks.py:183
-#, python-brace-format
-msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentário por [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/bitbucket/event_hooks.py:194
-#, python-brace-format
-msgid ""
-"Comment From BitBucket:\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentário pelo Bitbucket:\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/github/event_hooks.py:97
-#, python-brace-format
-msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
-msgstr ""
-"Status alterado por [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
-
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Status alterado por commit do Github."
-
-#: taiga/hooks/github/event_hooks.py:158
-#, python-brace-format
-msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-msgstr ""
-"Caso criado por [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Caso criado pelo Github."
-
-#: taiga/hooks/github/event_hooks.py:201
-#, python-brace-format
-msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentário por [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentário pelo Github:\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Status alterado por um commit de Gitlab"
-
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Criado pelo Gitlab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentário por [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Comentário pelo GitLab:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Ver projeto"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Ver marco de progresso"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
-msgid "View user stories"
-msgstr "Ver user stories"
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:26
+msgid "View user stories"
+msgstr "Ver histórias de usuário"
+
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Ver tarefa"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
-msgstr "Ver casos"
+msgstr "Ver problemas"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Ver página wiki"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Ver links wiki"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Solicitar filiação"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Adicionar user story para projeto"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Adicionar comentários para user story"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Adicionar comentário para tarefa"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Adicionar casos"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Adicionar comentários aos casos"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Adicionar página wiki"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "modificar página wiki"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Adicionar link wiki"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modificar wiki link"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Adicionar marco de progresso"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modificar marco de progresso"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Remover marco de progresso"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
-msgstr "Ver user story"
+msgstr "Ver história de usuário"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
-msgstr "Adicionar user story"
+msgstr "Adicionar história de usuário"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
-msgstr "Modificar user story"
+msgstr "Modificar história de usuário"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
-msgstr "Deletar user story"
+msgstr "Apagar história de usuário"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Adicionar tarefa"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modificar tarefa"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Deletar tarefa"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
-msgstr "Adicionar caso"
+msgstr "Adicionar problema"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
-msgstr "Modificar caso"
+msgstr "Modificar problema"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
-msgstr "Deletar caso"
+msgstr "Deletar problema"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Adicionar página wiki"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "modificar página wiki"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Deletar página wiki"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Adicionar link wiki"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modificar wiki link"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Deletar link wiki"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Modificar projeto"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Adicionar membro"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Remover membro"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Deletar projeto"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Adicionar membro"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Remover membro"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Valores projeto admin"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Funções Admin"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "dono"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Argumentos incompletos"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Formato de imagem inválida"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Nome de template inválido"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Descrição de template inválida"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
-msgstr ""
+msgstr "Id de usuário inválido"
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
-msgstr ""
+msgstr "O usuário não existe"
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
-msgstr ""
+msgstr "O usuário deve ser um membro do projeto"
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
+"O projeto deve ter um dono e pelo menos um dos usuários precisa ser um "
+"administrador ativo"
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Você não tem permissão para ver isso"
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Atualizações parciais não são suportadas"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "ID do projeto não combina entre objeto e projeto"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "projeto"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "tipo de conteúdo"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "identidade de objeto"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "data modificação"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "arquivo anexado"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "está obsoleto"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "ordem"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "Aparece em"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Personalizado"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
-msgstr ""
+msgstr "Este projeto está bloqueado por problemas de pagamento"
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
-msgstr ""
+msgstr "Este projeto está bloqueado por um administrador"
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
+msgstr "Este projeto está bloqueado porque o proprietário deixou o projeto"
+
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Texto"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Multi-linha"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Data"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
-msgstr ""
+msgstr "Url"
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "Tipo"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "valores"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
-msgid "user story"
-msgstr "user story"
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
+msgid "user story"
+msgstr "história de usuário"
+
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "tarefa"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
-msgstr "caso"
+msgstr "problema"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Já existe um com o mesmo nome."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "status"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "assunto"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "cor"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "assinado a"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "É requerimento do cliente"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "É requerimento do time"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Comentário já apagado"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Comentário não apagado"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Alterar"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Criar"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Apagar"
@@ -1608,7 +1651,7 @@ msgstr "removido"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Não-atribuído"
@@ -1655,95 +1698,76 @@ msgstr "De:"
msgid "To:"
msgstr "Para:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "conteúdo"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "nota bloqueada"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
-msgstr "Você não tem permissão para colocar esse sprint para esse caso."
+msgstr "Você não tem permissão para colocar essa sprint para esse problema."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
-msgstr "Você não tem permissão para colocar esse status para esse caso."
+msgstr "Você não tem permissão para colocar esse status para esse problema."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
-msgstr "Você não tem permissão para colocar essa severidade para esse caso."
+msgstr "Você não tem permissão para colocar essa gravidade para esse problema."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
-msgstr "Você não tem permissão para colocar essa prioridade para esse caso."
+msgstr ""
+"Você não tem permissão para colocar essa prioridade para esse problema."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
-msgstr "Você não tem permissão para colocar esse tipo para esse caso."
+msgstr "Você não tem permissão para colocar esse tipo para esse problema."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "status"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "severidade"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "prioridade"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "marco de progresso"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "data de término"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "assunto"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "assinado a"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "referência externa"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Curtir"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Curtidas"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -1755,8 +1779,9 @@ msgstr "data de início estimada"
msgid "estimated finish date"
msgstr "data de encerramento estimada"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "está fechado"
@@ -1768,290 +1793,384 @@ msgstr "disponibilidade"
msgid "The estimated start must be previous to the estimated finish."
msgstr "A estimativa de inicio deve ser anterior a estimativa de encerramento"
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Não há sprint com esse id"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "está bloqueado"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' parametro é mandatório"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project' parametro é mandatório"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "email"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "criado em"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "token"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "texto extra de convite"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "ordem de usuário"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "O usuário já é membro do projeto"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "pontos padrão"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "status de US padrão"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "pontos padrão"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "status padrão de tarefa"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "prioridade padrão"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "severidade padrão"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
-msgstr "status padrão de caso"
+msgstr "status padrão de problema"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
-msgstr "tipo padrão de caso"
+msgstr "tipo padrão de problema"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
-msgstr ""
+msgstr "logotipo"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "membros"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "total de marcos de progresso"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "pontos totais de US"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "painel de backlog ativo"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "painel de kanban ativo"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "painel de wiki ativo"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
-msgstr "painel de casos ativo"
+msgstr "painel de problemas ativo"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "sistema de vídeo conferência"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "informação extra de vídeo conferência"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "template de criação"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "permissão anônima"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "permissão de usuário"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "é privado"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "permissão anônima"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "permissão de usuário"
+
+#: taiga/projects/models.py:199
msgid "is featured"
-msgstr ""
+msgstr "é destaque"
+
+#: taiga/projects/models.py:202
+msgid "is looking for people"
+msgstr "está procurando colaboradores"
#: taiga/projects/models.py:204
-msgid "is looking for people"
-msgstr ""
-
-#: taiga/projects/models.py:206
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "cores de tags"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "data de atualização"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "contagem"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
-msgstr ""
+msgstr "atividades da última semana"
+
+#: taiga/projects/models.py:247
+msgid "activity last month"
+msgstr "atividades do último mês"
#: taiga/projects/models.py:250
-msgid "activity last month"
-msgstr ""
-
-#: taiga/projects/models.py:253
msgid "activity last year"
-msgstr ""
+msgstr "atividades do último ano"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "configurações de módulos"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "está arquivado"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "cor"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "trabalho no limite de progresso"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "função padrão para dono "
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "opções padrão"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "status de US"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "pontos"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "status de tarefa"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
-msgstr "status de casos"
+msgstr "status de problemas"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
-msgstr "tipos de caso"
+msgstr "tipos de problema"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "prioridades"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "severidades"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "funções"
-#: taiga/projects/notifications/choices.py:29
-msgid "Involved"
-msgstr ""
-
#: taiga/projects/notifications/choices.py:30
-msgid "All"
-msgstr ""
+msgid "Involved"
+msgstr "Envolvido"
#: taiga/projects/notifications/choices.py:31
-msgid "None"
-msgstr ""
+msgid "All"
+msgstr "Tudo"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/choices.py:32
+msgid "None"
+msgstr "Nada"
+
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "data de criação"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "histórico de entradas"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "notificar usuário"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Observado"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Existe notificação para usuário e projeto especifcado"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Valor inválido para nível de notificação"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2065,11 +2184,12 @@ msgid ""
" "
msgstr ""
"\n"
-" Caso atualizado
\n"
-" Olá %(user)s,
%(changer)s atualizou caso em %(project)s
\n"
-" Caso #%(ref)s %(subject)s
\n"
-" Ver caso\n"
+" Problema atualizado
\n"
+" Olá %(user)s,
%(changer)s atualizou um problema em %(project)s"
+"p>\n"
+"
Problema #%(ref)s %(subject)s
\n"
+" Ver problema\n"
"\n"
" "
@@ -2082,9 +2202,9 @@ msgid ""
"See issue #%(ref)s %(subject)s at %(url)s\n"
msgstr ""
"\n"
-"Caso atualizado\n"
-"Olá %(user)s, %(changer)s atualizou um caso em %(project)s\n"
-"Ver caso #%(ref)s %(subject)s em %(url)s\n"
+"Problema atualizado\n"
+"Olá %(user)s, %(changer)s atualizou um problema em %(project)s\n"
+"Ver problema #%(ref)s %(subject)s em %(url)s\n"
#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1
#, python-format
@@ -2093,7 +2213,7 @@ msgid ""
"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n"
msgstr ""
"\n"
-"[%(project)s] Atualizou o caso #%(ref)s \"%(subject)s\"\n"
+"[%(project)s] Atualização do problema #%(ref)s \"%(subject)s\"\n"
#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4
#, python-format
@@ -2109,12 +2229,13 @@ msgid ""
" "
msgstr ""
"\n"
-" Novo caso criado
\n"
-" Olá %(user)s,
%(changer)s criou um novo caso em %(project)s
\n"
-" Caso #%(ref)s %(subject)s
\n"
-" Ver caso\n"
-" O Time Taiga
\n"
+" Novo problema criado
\n"
+" Olá %(user)s,
%(changer)s criou um novo problema em %(project)s"
+"p>\n"
+"
Problema #%(ref)s %(subject)s
\n"
+" Ver problema\n"
+" Time Taiga
\n"
" "
#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1
@@ -2129,12 +2250,12 @@ msgid ""
"The Taiga Team\n"
msgstr ""
"\n"
-"Novo caso criado\n"
-"Olá %(user)s, %(changer)s criou um novo caso em %(project)s\n"
-"Ver caso #%(ref)s %(subject)s em %(url)s\n"
+"Novo problema criado\n"
+"Olá %(user)s, %(changer)s criou um novo problema em %(project)s\n"
+"Ver problema #%(ref)s %(subject)s em %(url)s\n"
"\n"
"---\n"
-"O Time Taiga\n"
+"Time Taiga\n"
#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1
#, python-format
@@ -2143,7 +2264,7 @@ msgid ""
"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n"
msgstr ""
"\n"
-"[%(project)s] Criou o caso #%(ref)s \"%(subject)s\"\n"
+"[%(project)s] Criação do problema #%(ref)s \"%(subject)s\"\n"
#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4
#, python-format
@@ -2157,10 +2278,10 @@ msgid ""
" "
msgstr ""
"\n"
-" Caso apagado
\n"
-" Olá %(user)s,
%(changer)s apagou um caso em %(project)s
\n"
-" Caso #%(ref)s %(subject)s
\n"
-" O Time Taiga
\n"
+" Problema apagado
\n"
+" Olá %(user)s,
%(changer)s apagou um problema em %(project)s
\n"
+" Problema #%(ref)s %(subject)s
\n"
+" Time Taiga
\n"
" "
#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1
@@ -2175,12 +2296,12 @@ msgid ""
"The Taiga Team\n"
msgstr ""
"\n"
-"Caso apagado\n"
-"Olá %(user)s, %(changer)s apagou um caso em %(project)s\n"
-"caso #%(ref)s %(subject)s\n"
+"Problema apagado\n"
+"Olá %(user)s, %(changer)s apagou um problema em %(project)s\n"
+"Problema #%(ref)s %(subject)s\n"
"\n"
"---\n"
-"O Time Taiga\n"
+"Time Taiga\n"
#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1
#, python-format
@@ -2189,7 +2310,7 @@ msgid ""
"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n"
msgstr ""
"\n"
-"[%(project)s] Apagou o caso #%(ref)s \"%(subject)s\"\n"
+"[%(project)s] Apagado o problema #%(ref)s \"%(subject)s\"\n"
#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4
#, python-format
@@ -2204,8 +2325,8 @@ msgid ""
" "
msgstr ""
"\n"
-" Sprint atualizado
\n"
-" Olá %(user)s,
%(changer)s atualizou um sprint em %(project)s"
+"
Sprint atualizada
\n"
+" Olá %(user)s,
%(changer)s atualizou uma sprint em %(project)s"
"p>\n"
"
Sprint %(name)s
\n"
" User Story atualizada\n"
-" Olá %(user)s,
%(changer)s atualizou a user story em %(project)s"
-"p>\n"
-"
User Story #%(ref)s %(subject)s
\n"
-" Ver user story\n"
+" História de Usuário atualizada
\n"
+" Olá %(user)s,
%(changer)s atualizou a história de usuário em "
+"%(project)s
\n"
+" História de Usuário #%(ref)s %(subject)s
\n"
+" Ver hstória de usuário\n"
" "
#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3
@@ -2498,9 +2619,9 @@ msgid ""
"See user story #%(ref)s %(subject)s at %(url)s\n"
msgstr ""
"\n"
-"User story atualizada\n"
-"Olá %(user)s, %(changer)s atualizou a user story em %(project)s\n"
-"Ver user story #%(ref)s %(subject)s em %(url)s\n"
+"História de usuário atualizada\n"
+"Olá %(user)s, %(changer)s atualizou a história de usuário em %(project)s\n"
+"Ver história de usuário #%(ref)s %(subject)s em %(url)s\n"
#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1
#, python-format
@@ -2509,7 +2630,7 @@ msgid ""
"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n"
msgstr ""
"\n"
-"[%(project)s] Atualizou a US #%(ref)s \"%(subject)s\"\n"
+"[%(project)s] Atualização da História de Usuário #%(ref)s \"%(subject)s\"\n"
#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4
#, python-format
@@ -2525,13 +2646,13 @@ msgid ""
" "
msgstr ""
"\n"
-" Nova user story criada
\n"
-" Olá %(user)s,
%(changer)s criou nova user story em %(project)s"
-"p>\n"
-"
User Story #%(ref)s %(subject)s
\n"
-" Ver user story\n"
-" O Time Taiga
\n"
+" Nova história de usuário criada
\n"
+" Olá %(user)s,
%(changer)s criou nova história de usuário em "
+"%(project)s
\n"
+" História de Usuário #%(ref)s %(subject)s
\n"
+" Ver história de usuário\n"
+" Time Taiga
\n"
" "
#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1
@@ -2546,12 +2667,12 @@ msgid ""
"The Taiga Team\n"
msgstr ""
"\n"
-"Nova user story criada\n"
-"Olá %(user)s, %(changer)s criou nova user story em %(project)s\n"
-"Ver user story #%(ref)s %(subject)s em %(url)s\n"
+"Nova história de usuário criada\n"
+"Olá %(user)s, %(changer)s criou nova história de usuário em %(project)s\n"
+"Ver história de usuário #%(ref)s %(subject)s em %(url)s\n"
"\n"
"---\n"
-"O Time Taiga\n"
+"Time Taiga\n"
#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1
#, python-format
@@ -2574,11 +2695,11 @@ msgid ""
" "
msgstr ""
"\n"
-" User Story apagada
\n"
-" Olá %(user)s,
%(changer)s apagou uma user story em %(project)s"
-"p>\n"
-"
User Story #%(ref)s %(subject)s
\n"
-" O Time Taiga
\n"
+" História de Usuário apagada
\n"
+" Olá %(user)s,
%(changer)s apagou uma história de usuário em "
+"%(project)s
\n"
+" História de Usuário #%(ref)s %(subject)s
\n"
+" Time Taiga
\n"
" "
#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1
@@ -2593,12 +2714,12 @@ msgid ""
"The Taiga Team\n"
msgstr ""
"\n"
-"User Story apagada\n"
-"Olá %(user)s, %(changer)s apagou user story em %(project)s\n"
-"User Story #%(ref)s %(subject)s\n"
+"História de Usuário apagada\n"
+"Olá %(user)s, %(changer)s apagou história de usuário em %(project)s\n"
+"História de Usuário #%(ref)s %(subject)s\n"
"\n"
"---\n"
-"O Time Taiga\n"
+"Time Taiga\n"
#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1
#, python-format
@@ -2654,7 +2775,7 @@ msgid ""
"[%(project)s] Updated the Wiki Page \"%(page)s\"\n"
msgstr ""
"\n"
-"[%(project)s] Atualizou a página wiki \"%(page)s\"\n"
+"[%(project)s] Atualização da página wiki \"%(page)s\"\n"
#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4
#, python-format
@@ -2762,159 +2883,185 @@ msgstr ""
"\n"
"[%(project)s] Apagou a página Wiki \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Observadores contém usuários inválidos"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "A versão precisa ser um inteiro"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "O parâmetro da versão não é válido"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "A versão não corresponde com a atual"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "versão"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
+"Você não pode deixar o projeto se você é o dono é não há outros "
+"administradores"
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Endereço de e-mail já utilizado"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Função inválida para projeto"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Opções padrão"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Status de user story"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Pontos"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Status de tarefas"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Status de casos"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Tipos de casos"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Prioridades"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Severidades"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Funções"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
+msgstr "Você atingiu o seu limite atual de membros para projetos privados"
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
+msgstr "Você atingiu o seu limite atual de membros para projetos públicos"
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
-msgstr ""
+msgstr "Você não pode ter mais projetos privados"
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
+"Este projeto atingiu o seu limite atual de membros para projetos privados"
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
-msgstr ""
+msgstr "Você não pode ter mais projetos públicos"
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
+"Este projeto atingiu o seu limite atual de membros para projetos públicos"
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Sprint futuro"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Fim do projeto"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Token é inválido"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
+msgstr "Token expirou"
+
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "tags"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "cores de tags"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "Você não tem permissão para colocar esse sprint para essa tarefa."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
-msgstr "Você não tem permissão para colocar essa user story para essa tarefa."
+msgstr ""
+"Você não tem permissão para colocar essa história de usuário para essa "
+"tarefa."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "Você não tem permissão para colocar esse status para essa tarefa."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "ordenar por US"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "ordenar por quadro de tarefa"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "é Iocaine"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Não há tarefas com esse id"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3074,11 +3221,15 @@ msgid ""
"new project owner for \"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+"Olá %(old_owner_name)s,
\n"
+"%(new_owner_name)s aceitou sua oferta e será o novo dono do projeto "
+"\"%(project_name)s\".
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
#, python-format
msgid "%(new_owner_name)s says:
"
-msgstr ""
+msgstr "%(new_owner_name)s diz:
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
@@ -3100,7 +3251,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
-msgstr ""
+msgstr "%(new_owner_name)s diz:"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
@@ -3116,6 +3267,8 @@ msgid ""
"\n"
"The Taiga Team\n"
msgstr ""
+"\n"
+"Time Taiga\n"
#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
#, python-format
@@ -3123,6 +3276,8 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer offer accepted!\n"
msgstr ""
+"\n"
+"[%(project)s] Oferta de transferência de propriedade de projeto aceita!\n"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
#, python-format
@@ -3141,6 +3296,9 @@ msgid ""
" %(rejecter_name)s says:
\n"
" "
msgstr ""
+"\n"
+" %(rejecter_name)s diz:
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
@@ -3167,7 +3325,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
msgid "%(rejecter_name)s says:"
-msgstr ""
+msgstr "%(rejecter_name)s diz:"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
@@ -3208,7 +3366,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
msgid "Continue"
-msgstr ""
+msgstr "Continuar"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
#, python-format
@@ -3275,7 +3433,7 @@ msgstr ""
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
#, python-format
msgid "%(owner_name)s says:"
-msgstr ""
+msgstr "%(owner_name)s diz:"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
@@ -3296,12 +3454,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3317,12 +3475,12 @@ msgstr ""
"se no processo que é compreendido sobre o produto e seus clientes."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3336,305 +3494,395 @@ msgstr ""
"uma lista."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Novo"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Pronto"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "Em andamento"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Pronto para teste"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Terminado"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Arquivado"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Fechado"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Precisa de informação"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Adiado"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Rejeitado"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Bug"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Pergunta"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Melhoria"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Baixa"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Alta"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Desejável"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Secundário"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Importante"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Crítica"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Front"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Back"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Product Owner"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Stakeholder"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
-msgstr "Você não tem permissão para colocar esse sprint para essa user story."
+msgstr ""
+"Você não tem permissão para colocar esse sprint para essa história de "
+"usuário."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
-msgstr "Você não tem permissão para colocar esse status para essa user story."
+msgstr ""
+"Você não tem permissão para colocar esse status para essa história de "
+"usuário."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
#, python-brace-format
-msgid "Generating the user story #{ref} - {subject}"
+msgid "Invalid role id '{role_id}'"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
+#, python-brace-format
+msgid "Generating the user story #{ref} - {subject}"
+msgstr "Gerando a história de usuário #{ref} - {subject}"
+
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "função"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "ordem do backlog"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "ordem do sprint"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "data de término"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "É requerimento do cliente"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "É requerimento do time"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
-msgstr "Gerado do caso"
+msgstr "Gerado do problema"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
-msgstr "Não há user story com esse id"
+msgstr "Não há história de usuário com esse id"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Não há projeto com esse id"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Não há status de user story com este id"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Endereço de e-mail já utilizado"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Não há status de tarega com este id"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Função inválida para projeto"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr "O dono do projeto deve ser um administrador."
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+"Pelo menos one dos usuários deve ser um administrador ativo neste projeto."
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Opções padrão"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Status de história de usuário"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Pontos"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Status de tarefas"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Status de problemas"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Tipos de problemas"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioridades"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Severidades"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Funções"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Votos"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Vote"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "parâmetro 'conteúdo' é mandatório"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "parametro 'project_id' é mandatório"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "último modificador"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Verifique o histórico da API para a exata diferença"
-#: taiga/users/admin.py:38
-msgid "Project Member"
-msgstr ""
-
#: taiga/users/admin.py:39
-msgid "Project Members"
-msgstr ""
+msgid "Project Member"
+msgstr "Membro do Projeto"
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:40
+msgid "Project Members"
+msgstr "Membros do Projeto"
+
+#: taiga/users/admin.py:50
msgid "id"
-msgstr ""
+msgstr "id"
#: taiga/users/admin.py:81
msgid "Project Ownership"
@@ -3654,60 +3902,60 @@ msgstr "Permissões"
#: taiga/users/admin.py:123
msgid "Restrictions"
-msgstr ""
+msgstr "Restrições"
#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Datas importantes"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "E-mail duplicado"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Não é um e-mail válido"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Usuário ou e-mail inválido"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "E-mail enviado com sucesso"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Parâmetro de senha atual necessário"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Parâmetro de nova senha necessário"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Comprimento de senha inválido, pelo menos 6 caracteres necessários"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Senha atual inválida"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Inválido, você está certo que o token está correto e não foi usado "
"anteriormente?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Inválido, tem certeza que o token está correto?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "status de superuser"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3715,24 +3963,24 @@ msgstr ""
"Designa que esse usuário tem todas as permissões sem explicitamente assiná-"
"las"
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "usuário"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Requerido. 30 caracteres ou menos. Letras, números e caracteres /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Digite um usuário válido"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "ativo"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3740,71 +3988,63 @@ msgstr ""
"Designa quando esse usuário deve ser tratado como ativo. desmarque isso em "
"vez de deletar contas."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografia"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "data ingressado"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "lingua padrão"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "tema padrão"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "fuso horário padrão"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "tags coloridas"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "token de e-mail"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "novo endereço de email"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "permissões"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "inválido"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Usuário inválido. Tente com um diferente."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Usuário ou senha não correspondem ao usuário"
@@ -3850,7 +4090,7 @@ msgstr ""
"Você pode ignorar essa mensagem caso não tenha solicitado\n"
"\n"
"---\n"
-"O Time Taiga\n"
+"Time Taiga\n"
#: taiga/users/templates/emails/change_email-subject.jinja:1
msgid "[Taiga] Change email"
@@ -3992,48 +4232,52 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Você foi Taigatizado!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Não há função com esse id"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "inválido"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Usuário inválido. Tente com um diferente."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
"Valor de chave duplicada viola regra de limitação. Chave '{}' já existe."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "chave"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "chave secreta"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "código de status"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "dados da requisição"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "cabeçalhos da requisição"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "dados de resposta"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "cabeçalhos de resposta"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duração"
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index 366ee566..e4d316a6 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/LC_MESSAGES/django.po
@@ -7,6 +7,7 @@
# Dmitriy Volkov , 2015
# Dmitry Lobanov , 2015
# Dmitry Vinokurov , 2015
+# Egor Poderyagin , 2016
# Igor Bezukladnikov , 2016
# ilyar, 2016
# ivan tkachenko , 2016
@@ -15,8 +16,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@@ -28,157 +29,161 @@ msgstr ""
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Публичная регистрация отключена."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "неправильный тип регистрации"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "неправильный тип логина"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Это имя уже используется."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "Этот адрес почты уже используется."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Токен не подходит ни под одно корректное приглашение."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Пользователь уже зарегистрирован."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "Этот пользователь уже является участником данного проекта"
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Ошибка при создании нового пользователя."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Неверный токен"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "неправильное имя пользователя"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Обязательно. 255 символов или меньше. Буквы, числа и символы /./-/_"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Это имя уже используется."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "Этот адрес почты уже используется."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Токен не подходит ни под одно корректное приглашение."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Пользователь уже зарегистрирован."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "Этот пользователь уже является участником данного проекта"
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Ошибка при создании нового пользователя."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Неверный токен"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Это поле обязательно."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Неправильное значение."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "значение '%s' должно быть True - верно - или False - ложно."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Введите корректное 'ссылочное имя' состоящее из букв, чисел, подчёркиваний и "
"дефисов."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Выберите правильное значение. %(value)s не является одним из доступных "
"значений."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Введите правильный адрес email."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "Дата имеет неверный формат. Воспользуйтесь одним из этих форматов: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr ""
"Дата и время имеют неправильный формат. Воспользуйтесь одним из этих "
"форматов: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr ""
"Время имеет неправильный формат. Воспользуйтесь одним из этих форматов: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Введите целое число."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Убедитесь, что это значение меньше или равно %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Убедитесь, что это значение больше или равно %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" значение должно быть числом с плавающей точкой."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Введите число."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Убедитесь, что здесь всего не больше %s цифр."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Убедитесь, что здесь не больше %s цифр после точкой."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Убедитесь, что здесь не больше %s цифр перед точкой."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Файл не был отправлен. Проверьте тип кодировки на форме."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Файл не был отправлен."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Отправленный файл пуст."
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -186,11 +191,11 @@ msgstr ""
"Убедитесь, что имя этого файла имеет не больше %(max)d букв (сейчас - "
"%(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "Пожалуйста, или отправьте файл, или снимите флажок."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -198,181 +203,178 @@ msgstr ""
"Загрузите корректное изображение. Файл, который вы загрузили - либо не "
"изображение, либо не корректное изображение."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
-msgstr ""
+msgstr "Заблокированный элемент"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Страница не является 'последней' и не может быть приведена к int."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Неправильная страница (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Неправильное определение разрешения"
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Неправильное значение ключа '%s' - объект не существует."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Неверный тип. Ожидалось значение ключа, пришло %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Объект с %s=%s не существует."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Неправильная гиперссылка - нет подходящего URL"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Неправильная гиперссылка - URL не подходит"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Неправильная гиперссылка из-за ошибки конфигурации"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Неправильная ссылка - объект не существует."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Неверный тип. Ожидалась строка URL, получено %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Неправильные данные."
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Ввод отсутствует"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Нельзя создать новые объект, только существующие объекты могут быть изменены."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Ожидался список объектов."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Не найдено"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Доступ запрещён"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Ошибка приложения на сервере"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Ошибка соединения."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Неверно сформированный запрос."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Неверные данные для аутентификации."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Данные для аутентификации не предоставлены."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "У вас нет разрешения для этого действия."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Метод '%s' не разрешён."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Не удалось соответствовать заголовку принятия для этого запроса"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Не поддерживаемый тип медиа '%s' в запросе."
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Запрос был замят"
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Будет доступно в течение %d секунд%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Неожиданная ошибка"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Не найдено."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Метод не поддерживается с этого конца."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Неправильные аргументы."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Ошибка при проверке данных"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Ошибка целостности из-за неправильных параметров"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Ошибка предусловия"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
-msgstr ""
+msgstr "Не осталось места для проектов"
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Ошибка в типах фильтров для параметров."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' должно быть целым значением."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "тэги"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -427,7 +429,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -439,27 +441,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-" ПоддержкаTaiga:"
-"strong>\n"
-" %(support_url)s"
-"a>\n"
-"
\n"
-" Свяжитесь с нами:"
-"\n"
-" \n"
-" %(support_email)s\n"
-" \n"
-"
\n"
-" Рассылка:"
-"strong>\n"
-" \n"
-" %(mailing_list_url)s\n"
-" \n"
-" "
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -517,103 +498,88 @@ msgstr ""
" Комментарий: %(comment)s\n"
" "
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Нам была нужна хотя бы одна роль"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
-msgstr "Необходим дамп-файл"
+msgstr "Необходим дамп"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Неправильный формат дампа"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" не найдено в этом проекте"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Неправильные данные. Должны быть в формате {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Содержит неверные специальные поля"
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Уже есть такое имя для проекта"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "ошибка при импорте данных проекта"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "ошибка при импорте ролей"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "ошибка при импорте членства"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "ошибка при импорте списков свойств проекта"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "ошибка при импорте значений по умолчанию свойств проекта"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "ошибка при импорте пользовательских свойств"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "ошибка при импорте спринтов"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "ошибка импорта историй от пользователей"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "ошибка импорта задач"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "ошибка при импорте запросов"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "ошибка импорта историй от пользователей"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "ошибка импорта задач"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "ошибка при импорте вики-страниц"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "ошибка при импорте вики-ссылок"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "ошибка импорта тэгов"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "ошибка импорта хронологии проекта"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
-msgstr ""
+msgstr "неожиданная ошибка импортирования проекта"
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Ошибка создания свалочного файла для проекта"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -632,18 +598,33 @@ msgid ""
"TRACE ERROR:\n"
"------------"
msgstr ""
+"\n"
+"\n"
+"Ошибка загрузки дампа {user_full_name} <{user_email}>:\"\n"
+"\n"
+"\n"
+"ПРИЧИНА:\n"
+"-------\n"
+"{reason}\n"
+"\n"
+"ДЕТАЛИ:\n"
+"--------\n"
+"{details}\n"
+"\n"
+"ТРАССИРОВКА ОШИБКИ:\n"
+"------------"
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
-msgstr "Ошибка загрузки свалочного файла проекта"
+msgstr "Ошибка загрузки дампа"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
-msgstr ""
+msgstr "Ошибка загрузки дампа вашего проекта"
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
-msgstr ""
+msgstr "-- нет детальной информации --"
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
@@ -878,77 +859,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Дамп вашего проекта импортирован"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" не найдено в этом проекте"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Неправильные данные. Должны быть в формате {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Содержит неверные специальные поля"
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Уже есть такое имя для проекта"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Необходима аутентификация"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "имя"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "url иконки"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "веб"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "описание"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Следующий url"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "секретный ключ для шифрования токенов приложения"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "пользователь"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "приложение"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "полное имя"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "адрес email"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "комментарий"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "дата создания"
@@ -979,7 +980,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Дополнительное инфо"
@@ -1013,547 +1014,579 @@ msgstr ""
"\n"
"[Taiga] Отзыв от %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "Нагрузочный файл не является правильным json-файлом"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Проект не существует"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Плохая подпись"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Указанный элемент не существует"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Статус не существует"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Статус изменён из-за вклада с BitBucket"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Неверная информация о запросе"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-"Запрос создан [@{bitbucket_user_name}]({bitbucket_user_url} \"Посмотреть "
-"профиль @{bitbucket_user_name} на BitBucket\") на BitBucket.\n"
-"Изначальный запрос на BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Перейти к 'bb#{number} - {subject}'\"):\n"
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
"\n"
-"{description}"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Запрос создан из BitBucket."
-
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Неправильная информация в комментарии к запросу"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-"Комментарий от [@{bitbucket_user_name}]({bitbucket_user_url} \"Посмотреть "
-"профиль @{bitbucket_user_name} на BitBucket\") на BitBucket.\n"
-"Изначальный запрос на BitBucket: [bb#{number} - {subject}]({bitbucket_url} "
-"\"Перейти к 'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Неверная информация о запросе"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Комментарий от BitBucket:\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Статус изменён пользователем [@{github_user_name}]({github_user_url} "
-"\"Посмотреть профиль @{github_user_name} на GitHub\") из-за вклада на GitHub "
-"[{commit_id}]({commit_url} \"Посмотреть вклад '{commit_id} - "
-"{commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Статус изменён из-за вклада на GitHub."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"Запрос создана [@{github_user_name}]({github_user_url} \"Посмотреть профиль "
-"@{github_user_name} на GitHub\") из GitHub.\n"
-"Исходный запрос на GitHub: [gh#{number} - {subject}]({github_url} \"Перейти "
-"к 'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Запрос создан из GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-"Комментарий от [@{github_user_name}]({github_user_url} \"Посмотреть профиль "
-"@{github_user_name} на GitHub\") из GitHub.\n"
-"Исходный запрос на GitHub: [gh#{number} - {subject}]({github_url} \"Перейти "
-"к 'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"Комментарий из GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Указанный элемент не существует"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Статус изменён из-за вклада на GitLab"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Статус не существует"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Создано из GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Комментарий от [@{gitlab_user_name}]({gitlab_user_url} \"Посмотреть профиль "
-"@{gitlab_user_name} на GitLab\") из GitLab.\n"
-"Исходный запрос на GitLab: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Комментарий из GitLab:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Просмотреть проект"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Просмотреть вехи"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Просмотреть пользовательские истории"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Просмотреть задачи"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Посмотреть запросы"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Просмотреть wiki-страницы"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Просмотреть wiki-ссылки"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Запросить членство"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Добавить пользовательскую историю к проекту"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Добавить комментарии к пользовательским историям"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Добавить комментарии к задачам"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Добавить запросы"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Добавить комментарии к запросам"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Создать wiki-страницу"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Изменить wiki-страницу"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Добавить wiki-ссылку"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Изменить wiki-ссылку"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Добавить веху"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Изменить веху"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Удалить веху"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Просмотреть пользовательскую историю"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Добавить пользовательскую историю"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Изменить пользовательскую историю"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Удалить пользовательскую историю"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Добавить задачу"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Изменить задачу"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Удалить задачу"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Добавить запрос"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Изменить запрос"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Удалить запрос"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Создать wiki-страницу"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Изменить wiki-страницу"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Удалить wiki-страницу"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Добавить wiki-ссылку"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Изменить wiki-ссылку"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Удалить wiki-ссылку"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Изменить проект"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Добавить участника"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Удалить участника"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Удалить проект"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Добавить участника"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Удалить участника"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Управлять значениями проекта"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Управлять ролями"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "владелец"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Список аргументов неполон"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Неправильный формат изображения"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Неверное название шаблона"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Неверное описание шаблона"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
-msgstr ""
+msgstr "Неправильный id пользователя"
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
-msgstr ""
+msgstr "Пользователь не существует"
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
-msgstr ""
+msgstr "Пользователь должен быть участником проекта"
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
+"У проекта должен быть владелец и по крайней мере один пользователь должен "
+"быть активным администратором"
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "У вас нет разрешения на просмотр."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Частичные обновления не поддерживаются"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "Идентификатор проекта не подходит к этому объекту"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "проект"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "тип содержимого"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "идентификатор объекта"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "изменённая дата"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "приложенный файл"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "устаревшее"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "порядок"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Специальный"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
-msgstr ""
+msgstr "Проект заблокирован из-за ошибки при оплате"
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
-msgstr ""
+msgstr "Проект заблокирован администраторами"
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
+msgstr "Проект заблокирован, потому-что владелец ушёл"
+
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Текст"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Многострочный текст"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Дата"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
-msgstr ""
+msgstr "Url"
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "тип"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "значения"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "пользовательская история"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "задача"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "запрос"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Это имя уже используется."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "Ссылка"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "cтатус"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "тема"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "цвет"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "назначено"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "является требованием клиента"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "является требованием команды"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Комментарий уже был удалён"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Комментарий не удалён"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Изменить"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Создать"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Удалить"
@@ -1609,7 +1642,7 @@ msgstr "удалено"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Не назначено"
@@ -1656,99 +1689,79 @@ msgstr "От:"
msgid "To:"
msgstr "Кому:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "содержимое"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "Заметка о блокировке"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "спринт"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такой спринт для этого запроса"
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такой статус для этого запроса"
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такую важность для этого запроса"
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr ""
"У вас нет прав для того чтобы установить такой приоритет для этого запроса"
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "У вас нет прав для того чтобы установить такой тип для этого запроса"
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "Ссылка"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "cтатус"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "важность"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "приоритет"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "веха"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "дата завершения"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "тема"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "назначено"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "внешняя ссылка"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Лайк"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Лайки"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "ссылочное имя"
@@ -1760,8 +1773,9 @@ msgstr "предполагаемая дата начала"
msgid "estimated finish date"
msgstr "предполагаемая дата завершения"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "закрыто"
@@ -1775,290 +1789,384 @@ msgstr ""
"Предполагаемая дата начала должна предшествовать предполагаемой дате "
"завершения."
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Не существует спринта с таким идентификатором"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "заблокировано"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "параметр '{param}' является обязательным"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "параметр 'project' является обязательным"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "электронная почта"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "создано"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "идентификатор"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "дополнительный текст к приглашению"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "порядок пользователей"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "Этот пользователем уже является участником проекта"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "очки по умолчанию"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "статусы ПИ по умолчанию"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "очки по умолчанию"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "статус задачи по умолчанию"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "приоритет по умолчанию"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "важность по умолчанию"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "статус запроса по умолчанию"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "тип запроса по умолчанию"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "лготип"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "участники"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "общее количество вех"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "очки истории"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "активная панель списка задач"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "активная панель kanban"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "активная wiki-панель"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "панель активных запросов"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "система видеоконференций"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "дополнительные данные системы видеоконференций"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "шаблон для создания"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "права анонимов"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "права пользователя"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "личное"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "права анонимов"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "права пользователя"
+
+#: taiga/projects/models.py:199
msgid "is featured"
-msgstr ""
+msgstr "особенность"
+
+#: taiga/projects/models.py:202
+msgid "is looking for people"
+msgstr "ищут людей"
#: taiga/projects/models.py:204
-msgid "is looking for people"
-msgstr ""
-
-#: taiga/projects/models.py:206
msgid "loking for people note"
-msgstr ""
+msgstr "ищем замечания людей"
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "цвета тэгов"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
-msgstr ""
+msgstr "токен передачи проекта"
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
-msgstr ""
+msgstr "заблокированный код"
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "дата и время обновления"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "количество"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
-msgstr ""
+msgstr "фанатов на прошлой недели "
+
+#: taiga/projects/models.py:235
+msgid "fans last month"
+msgstr "фанатов в прошлом месяце"
#: taiga/projects/models.py:238
-msgid "fans last month"
-msgstr ""
-
-#: taiga/projects/models.py:241
msgid "fans last year"
-msgstr ""
+msgstr "фанатов в прошлом году"
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr "активность за неделю"
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr "активность за месяц"
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr "активность за год"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "конфигурация модулей"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "архивировано"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "цвет"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "ограничение на активную работу"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "значение"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "роль владельца по умолчанию"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "параметры по умолчанию"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "статусы ПИ"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "очки"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "статусы задач"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "статусы запросов"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "типы запросов"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "приоритеты"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "степени важности"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "роли"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Вовлеченные"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Все"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Никаких"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "дата и время создания"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "записи истории"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "уведомить пользователей"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Просмотренные"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Уведомление существует для данных пользователя и проекта"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Неверное значение для уровня уведомлений"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2777,160 +2885,181 @@ msgstr ""
"\n"
"[%(project)s] Удалена вики-страница \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "наблюдатели содержат неправильных пользователей"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "Версия должна быть целым значением"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "Значение версии некорректно"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Версия не соответствует текущей"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "версия"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
+"Вы не можете покинуть проект, если вы владелец или нет других администраторов"
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "Этот почтовый адрес уже используется"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Неверная роль для этого проекта"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Параметры по умолчанию"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Статусу пользовательских историй"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Очки"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Статусы задачи"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Статусы запроса"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Типы запроса"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Приоритеты"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Степени важности"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Роли"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
+msgstr "Вы достигли лимита участников для частного проекта"
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
+msgstr "Вы достигли лимита участников для публичного проекта"
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
-msgstr ""
+msgstr "Вы не можете иметь больше частных проектов"
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
-msgstr ""
+msgstr "В этом частном проекте достигнут лимит участников"
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
-msgstr ""
+msgstr "Вы не можете иметь больше публичных проектов"
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
-msgstr ""
+msgstr "В этом публичном проекте достигнут лимит участников"
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Будущий спринт"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Окончание проекта"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Неверный токен"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
+msgstr "Срок действия токена истёк"
+
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "тэги"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "цвета тэгов"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "У вас нет прав, чтобы назначить этот спринт для этой задачи."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"У вас нет прав, чтобы назначить эту историю от пользователя этой задаче."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "У вас нет прав, чтобы установить этот статус для этой задачи."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "порядок ПИ"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "порядок панели задач"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "- иокаин"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Нет задачи с таким идентификатором"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3092,11 +3221,16 @@ msgid ""
"new project owner for \"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+"Привет %(old_owner_name)s,
\n"
+"%(new_owner_name)s подтвердил ваше предложение и будет новым владельцем "
+"для \"%(project_name)s\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10
#, python-format
msgid "%(new_owner_name)s says:
"
-msgstr ""
+msgstr "%(new_owner_name)s сказал:
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
@@ -3105,6 +3239,9 @@ msgid ""
"p>\n"
" "
msgstr ""
+"\n"
+"С этого момента Ваш статус будет администратор.
\n"
+" "
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
#, python-format
@@ -3114,17 +3251,23 @@ msgid ""
"%(new_owner_name)s has accepted your offer and will become the new project "
"owner for \"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Привет %(old_owner_name)s,\n"
+"%(new_owner_name)s подтвердил ваше предложение и будет новым владельцем для "
+"\"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
msgid "%(new_owner_name)s says:"
-msgstr ""
+msgstr "%(new_owner_name)s сказал:"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11
msgid ""
"\n"
"From now on, your new status for this project will be \"admin\".\n"
msgstr ""
+"\n"
+"С этого момента Ваш статус будет администратор.\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19
@@ -3134,6 +3277,8 @@ msgid ""
"\n"
"The Taiga Team\n"
msgstr ""
+"\n"
+"The Taiga Team\n"
#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1
#, python-format
@@ -3141,6 +3286,8 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer offer accepted!\n"
msgstr ""
+"\n"
+"[%(project)s] Передача проекта подтверждена\n"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4
#, python-format
@@ -3151,6 +3298,10 @@ msgid ""
"new project owner for \"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+"Привет %(owner_name)s,
\n"
+"%(rejecter_name)s отменил ваше предложение и не будет новым владельцем "
+"для \"%(project_name)s\".
"
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10
#, python-format
@@ -3159,6 +3310,9 @@ msgid ""
" %(rejecter_name)s says:
\n"
" "
msgstr ""
+"\n"
+"%(rejecter_name)s сказал:
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16
msgid ""
@@ -3167,11 +3321,15 @@ msgid ""
"different person.\n"
" "
msgstr ""
+"\n"
+"Если Вы хотите, Вы можете попробовать передать собственность другой "
+"персоне.
\n"
+" "
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21
#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22
msgid "Request transfer to a different person"
-msgstr ""
+msgstr "Запрос передан другой персоне"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1
#, python-format
@@ -3181,11 +3339,15 @@ msgid ""
"%(rejecter_name)s has declined your offer and will not become the new "
"project owner for \"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Привет %(owner_name)s,\n"
+"%(rejecter_name)s отменил ваше предложение и не будет новым владельцем для "
+"\"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7
#, python-format
msgid "%(rejecter_name)s says:"
-msgstr ""
+msgstr "%(rejecter_name)s сказал:"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11
msgid ""
@@ -3193,10 +3355,13 @@ msgid ""
"If you want, you can still try to transfer the project ownership to a "
"different person.\n"
msgstr ""
+"\n"
+"Если Вы хотите, Вы можете попробовать передать собственность другой "
+"персоне.\n"
#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15
msgid "Request transfer to a different person:"
-msgstr ""
+msgstr "Запрос передан другой персоне:"
#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1
#, python-format
@@ -3204,6 +3369,8 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer declined\n"
msgstr ""
+"\n"
+"[%(project)s] Передача проекта отменена\n"
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4
#, python-format
@@ -3214,6 +3381,11 @@ msgid ""
"\"%(project_name)s\".\n"
" "
msgstr ""
+"\n"
+"Привет %(owner_name)s,
\n"
+"%(requester_name)s просит назначить его владельцем проекта для "
+"\"%(project_name)s\".
\n"
+" "
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9
msgid ""
@@ -3222,11 +3394,15 @@ msgid ""
"project transfer from the administration panel.\n"
" "
msgstr ""
+"\n"
+"Пожалуйста, нажмите \"Продолжить\" если вы хотите начать передачу проекта "
+"из панели администратора.
\n"
+" "
#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22
msgid "Continue"
-msgstr ""
+msgstr "Продолжить"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1
#, python-format
@@ -3236,6 +3412,10 @@ msgid ""
"%(requester_name)s has requested to become the project owner for "
"\"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Привет %(owner_name)s,\n"
+"%(requester_name)s просит назначить его владельцем проекта для "
+"\"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6
msgid ""
@@ -3243,10 +3423,13 @@ msgid ""
"Please, go to your project settings if you would like to start the project "
"transfer from the administration panel.\n"
msgstr ""
+"\n"
+"Пожалуйста, перейдите в настройки проекта, если Вы хотите начать передачу "
+"проекта из панели администратора.\n"
#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10
msgid "Go to your project settings:"
-msgstr ""
+msgstr "Перейдите в настройки проекта:"
#: taiga/projects/templates/emails/transfer_request-subject.jinja:1
#, python-format
@@ -3254,6 +3437,8 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer request\n"
msgstr ""
+"\n"
+"[%(project)s] Запрос передачи проекта\n"
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4
#, python-format
@@ -3264,6 +3449,11 @@ msgid ""
"would like you to become the new project owner.\n"
" "
msgstr ""
+"\n"
+"Привет %(receiver_name)s,
\n"
+"%(owner_name)s, текущий владелец \"%(project_name)s\", хотел что бы Вы "
+"стали новым владельцем проекта.
\n"
+" "
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10
#, python-format
@@ -3272,6 +3462,9 @@ msgid ""
" %(owner_name)s says:
\n"
" "
msgstr ""
+"\n"
+"%(owner_name)s сказал:
\n"
+" "
#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17
msgid ""
@@ -3280,6 +3473,10 @@ msgid ""
"proposal.\n"
" "
msgstr ""
+"\n"
+"Пожалуйста, нажмите \"Продолжить\", для принятия или отклонения "
+"предложения
\n"
+" "
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1
#, python-format
@@ -3289,11 +3486,15 @@ msgid ""
"%(owner_name)s, the current project owner at \"%(project_name)s\" would like "
"you to become the new project owner.\n"
msgstr ""
+"\n"
+"Привет %(receiver_name)s,\n"
+"%(owner_name)s, владелец \"%(project_name)s\", хотел что бы Вы стали новым "
+"владельцем проекта.\n"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6
#, python-format
msgid "%(owner_name)s says:"
-msgstr ""
+msgstr "%(owner_name)s сказал:"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11
msgid ""
@@ -3301,10 +3502,13 @@ msgid ""
"Please, go to the following link to either accept or reject this proposal."
"p>\n"
msgstr ""
+"\n"
+"Пожалуйста, пройдите по следующей ссылке для принятия или отклонения "
+"предложения.\n"
#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15
msgid "Accept or reject the project ownership transfer:"
-msgstr ""
+msgstr "Подтвердите или отклоните передачу владения проектом:"
#: taiga/projects/templates/emails/transfer_start-subject.jinja:1
#, python-format
@@ -3312,14 +3516,16 @@ msgid ""
"\n"
"[%(project)s] Project ownership transfer offer\n"
msgstr ""
+"\n"
+"[%(project)s] Предложение передачи проекта\n"
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3336,12 +3542,12 @@ msgstr ""
"известно о продукте и его пользователях."
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3355,315 +3561,402 @@ msgstr ""
"задачи из очереди."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Новая"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Готово"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "В процессе"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Можно проверять"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Завершена"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Архивирована"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Закрыта"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Требуются подробности"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Отложено"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Отклонена"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Ошибка"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Вопрос"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Улучшение"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Низкий"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Обычный"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Высокий"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Список пожеланий"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Низкий"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Важный"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Критический"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "Юзабилити"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Дизайнер"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Фронтенд разработчик"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Бэкенд разработчик"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Владелец продукта"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Заинтересованная сторона"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"У вас нет прав чтобы установить спринт для этой пользовательской истории."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"У вас нет прав чтобы установить статус для этой пользовательской истории."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Генерируется пользовательская история #{ref} - {subject}"
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "роль"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "порядок списка задач"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "порядок спринтов"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "дата окончания"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "является требованием клиента"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "является требованием команды"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "создано из запроса"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Не существует пользовательской истории с таким идентификатором"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Не существует проекта с таким идентификатором"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Не существует статуса пользовательской истории с таким идентификатором"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "Этот почтовый адрес уже используется"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Не существует статуса задачи с таким идентификатором"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Неверная роль для этого проекта"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr "Владелец проекта должен быть администратором"
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+"По крайней мере один пользователь должен быть администратором для этого "
+"проекта"
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Параметры по умолчанию"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Статусу пользовательских историй"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Очки"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Статусы задачи"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Статусы запроса"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Типы запроса"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Приоритеты"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Степени важности"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Роли"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Голоса"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Голосовать"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "параметр 'content' является обязательным"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "параметр 'project_id' является обязательным"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "последний отредактировавший"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Свертесть с историей API для получения изменений"
-#: taiga/users/admin.py:38
-msgid "Project Member"
-msgstr ""
-
#: taiga/users/admin.py:39
-msgid "Project Members"
-msgstr ""
+msgid "Project Member"
+msgstr "Участник проекта"
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:40
+msgid "Project Members"
+msgstr "Участники проекта"
+
+#: taiga/users/admin.py:50
msgid "id"
-msgstr ""
+msgstr "id"
#: taiga/users/admin.py:81
msgid "Project Ownership"
-msgstr ""
+msgstr "Владелец проекта"
#: taiga/users/admin.py:82
msgid "Project Ownerships"
-msgstr ""
+msgstr "Владельцы проекта"
#: taiga/users/admin.py:119
msgid "Personal info"
@@ -3675,151 +3968,143 @@ msgstr "Права доступа"
#: taiga/users/admin.py:123
msgid "Restrictions"
-msgstr ""
+msgstr "Ограничения"
#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Важные даты"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "Этот email уже используется"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Невалидный email"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Неверное имя пользователя или e-mail"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Письмо успешно отправлено!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Поле \"текущий пароль\" является обязательным"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Поле \"новый пароль\" является обязательным"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Неверная длина пароля, требуется как минимум 6 символов"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Неверно указан текущий пароль"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr "Неверно, вы уверены что токен правильный и не использовался ранее?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Неверно, вы уверены что токен правильный?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "статус суперпользователя"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr "Выбранный пользователь имеет все разрешения, ему не чего назначит."
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "имя пользователя"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "Обязательно. 30 символов или меньше. Буквы, числа и символы /./-/_"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Введите корректное имя пользователя."
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "активный"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr "Выбранный пользователь активен. Отменить выбор для удаления аккаунта."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "биография"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "фотография"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "когда присоединился"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "язык по умолчанию"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "тема по умолчанию"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "временная зона по умолчанию"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "установить цвета для тэгов"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "email токен"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "новый email адрес"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
-msgstr ""
+msgstr "максимальное число частных проектов"
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
-msgstr ""
+msgstr "максимальное число публичных проектов"
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
-msgstr ""
+msgstr "максимальное число участников для каждого частного проекта"
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
-msgstr ""
+msgstr "максимальное число участников для каждого публичного проекта"
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "разрешения"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "невалидный"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Неверное имя пользователя. Попробуйте другое."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Имя пользователя или пароль не соответствуют пользователю."
@@ -4013,48 +4298,52 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Вы в Тайге!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Не существует роли с таким идентификатором"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "невалидный"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Неверное имя пользователя. Попробуйте другое."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
"Дублирующий ключ, значение должно быть уникальны. Ключ '{}' уже существует."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "ключ"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "Секретный ключ"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "код статуса"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "данные запроса"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "заголовки запроса"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "данные ответа"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "заголовки ответа"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "длительность"
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 705d5cc6..09cbdb49 100644
--- a/taiga/locale/sv/LC_MESSAGES/django.po
+++ b/taiga/locale/sv/LC_MESSAGES/django.po
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -19,154 +19,158 @@ msgstr ""
"Language: sv\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "Publikt register är avvaktiverad."
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "Felaktigt registertyp"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "Invalid inloggningstyp"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Användarnamnet används redan"
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "E-postadressen används redan"
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Förekomsten passar inte invitationen. "
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Användaren finns redan."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr ""
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Ett fel uppstod når användaren skapades. "
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Felaktig förekomst. "
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "Felaktigt användarnamn"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "Kräver färre än 255 tecken. Kan vara tecken, nummer och /./-/_."
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Användarnamnet används redan"
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "E-postadressen används redan"
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Förekomsten passar inte invitationen. "
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Användaren finns redan."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr ""
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Ett fel uppstod når användaren skapades. "
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Felaktig förekomst. "
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Fältet är obligatoriskt."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Felaktigt värde. "
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' värdet måste vara sann eller falskt. "
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Skriv in ett giltigt 'slugg' som består av bokstäver, nummer, understreck "
"och bindestreck."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "Välj korrekt. %(value)s är inte ett giltigt val. "
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Skriv in en giltig e-postadress"
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "Felaktigt datumformat. Använd ett av dessa formaten istället: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "Tidsdatum har fel format. Bruk ett av dessa formaten istället: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "Felaktigt tidsformat. Bruk ett av dessa formaten istället: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Skriv ett helt nummer."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "Försäkra dig om att värdet är mindre eller lika med %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "Försäkra dig om att värdet är större eller lika med %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" värde måste vara flyttal."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Skriv in ett nummer."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Försäkra dig om att det inge är mera än %s siffror i totalen. "
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Försäkra dig om att det inte är mera än %s decimaler."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr ""
"Försäkra dig om det inte är mera än %s siffror till vänster om "
"decimalpunkten."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Inga filer skickades. Check kodningstypen på formularet. "
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Skickade ingen fil. "
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "Den insända filen är tom. "
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -174,12 +178,12 @@ msgstr ""
"Försäkra dig om att filnamnet har som mest %(max)d tecken (det har "
"%(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Vänligen lämna in en fil eller kontrollera kryssrutan för klar, inte båda."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -187,182 +191,179 @@ msgstr ""
"Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild "
"eller en skadad bild."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr ""
"Sidan är inte \"sist\", och inte heller kan den omvandlas till ett heltal."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Felaktig sida (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Ogiltigt definition för behörighet."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Ogiltigt paket '%s' - objektet existerar inte."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Ogiltigt typ. Förväntad paketvärde, mottaget %s. "
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "Objekt med %s=%s existerar inte. "
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Ogiltigt länkadress - Inga länkar passar."
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Ogiltigt länkadress - Felaktig matchning av länkar."
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Felaktig länk förorsakad av et konfigurationsfel. "
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Fel länk - objekten existerar inte. "
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Felaktigt typ. Förväntad länksträng, mottagit %s. "
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Felaktigt data"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Inga indata"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr ""
"Det går inte att skapa ett nytt objekt, endast befintliga poster uppdateras."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Förväntad lista på poster."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Hittade inte"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "Du har inte behöriget"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Serverprogramfel."
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Felaktigt förbindelse."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Felaktigt begäran"
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Felaktiga autentiseringsreferenser "
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Autentiseringsuppgifter lämnades inte."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Du har inte behörigheter för att utföra denna åtgärd. "
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "Metoden '%s' är inte tillåtet. "
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "Det gick inte att tillgodose begäran på Accept-huvudet"
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "Mediatypen '%s' du begär stöds inte. "
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "Begäran blev strypt."
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "Förväntas bli tillgängligt inom %d second%s."
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Oväntat fel"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Hittade inget"
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "Metoden stöds inte för denna slutpunkten."
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Fel argument."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Datavalideringsfel"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Integritetsfel för felaktiga eller ogiltiga argument"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Förutsättningsfel"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Fel i filterparametertyper."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'Projektet\" måste vara ett heltal."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "taggar"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -417,7 +418,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -429,22 +430,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-"Taiga Support:\n"
-""
-"%(support_url)s\n"
-"
\n"
-"Kontakt oss:\n"
-"\n"
-"%(support_email)s\n"
-"\n"
-"
\n"
-"E-postlista:\n"
-"\n"
-"%(mailing_list_url)s\n"
-""
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -491,103 +476,88 @@ msgid ""
" "
msgstr ""
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "Vi behöver minst en roll"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "Behöver en hämtningsfil"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Invalid hämtningsfilformat"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" gick inte att hitta för det här projektet"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Felaktigt innehåll. Det måste vara {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Innehåller felaktigt anpassad fält."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Namnet är upprepad för projektet"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "fel vid import av projektdata"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "fel vid importering av roller"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "fel vid import av medlemskap"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "fel vid import av en lista på projektegenskaper"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "fel vid import av standard projektegenskapsvärden"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "fel vid import av anpassade egenskaper"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "felaktig import av sprintar"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "fel vid import av användarhistorier"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "fel vid import av uppgifter"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "fel vid import av ärenden"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "fel vid import av användarhistorier"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "fel vid import av uppgifter"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "vel vid import av wiki-sidor"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "fel vid import av wiki-länkar"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "fel vid importering av taggar"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "fel vid importering av tidslinje"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Fel vid skapandet av projektkopia"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -607,15 +577,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Feil vid hämtning av projektkopia"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -766,77 +736,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Ditt projekt importerades korrekt"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" gick inte att hitta för det här projektet"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Felaktigt innehåll. Det måste vara {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Innehåller felaktigt anpassad fält."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Namnet är upprepad för projektet"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Verifiering krävs"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "namn"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "Ikonlänk"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "Internet"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "beskrivning"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Nästa länk"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "hemlig nyckel för kryptering av programtecken "
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "användare"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "program"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "hela namnet"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "e-postadress"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "kommentera"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "skapad datum"
@@ -859,7 +849,7 @@ msgid ""
msgstr ""
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Extra information"
@@ -885,515 +875,577 @@ msgid ""
"[Taiga] Feedback from %(full_name)s <%(email)s>\n"
msgstr ""
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "Datasträngen är inte korrekt json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Projektet existerar inte"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Dålig signatur"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Referenselementet existerar inte"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Statusen existerar inte"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Status ändrad från BitBucket skrivs in"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Felaktig ärendeinformation"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Ärende skapades från BitBucket."
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Felaktigt kommentarinformation för ärendet"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Felaktig ärendeinformation"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Status ändrad av [@{github_user_name}]({github_user_url} \"Se "
-"@{github_user_name}'s GitHub profil\") från GitHub commit [{commit_id}]"
-"({commit_url} \"Se bidrag '{commit_id} - {commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Status ändrad från GitHub inläggs."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "Ärende skapad från GitHub."
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Referenselementet existerar inte"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "Status ändrad från GitLab inlagd"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Statusen existerar inte"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "Skapad från GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-"Kommentar av [@{gitlab_user_name}]({gitlab_user_url} \"Se "
-"@{gitlab_user_name}'s GitLab profile\") från GitLab.\n"
-"\n"
-"Ursprunglig GitLab ärende: [gl#{number} - {subject}]({gitlab_url} \"Gå till "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Visa projekt"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Visa milstolper"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Visa användarhistorier"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Visa uppgifter"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Visa ärenden"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Visa wiki-sidor"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Visa wiki-länkar"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Ansöka om medlemskap"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Lägg till användarhistorie till projekt"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Lägg till kommentarer till användarhistorie"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Lägg till kommentar till uppgifter"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Lägg till ärenden"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Lägg till kommentar till ärender"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Lägg till en wiki-sida"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Modifiera wiki-sida"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Lägg till wiki-länk"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Modifiera wiki-link"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Lägg till milstolpe"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Modifiera milstolpe"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Ta bort milstolpe"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Visa användarhistorie"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Lägg till användarhistorie"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Modifiera användarhistorien"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Ta bort användarhistorien"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Lägg till uppgift"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Modifiera uppgift"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Ta bort uppgift"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Lägg till ärende"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Modifiera ärende"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Ta bort ärende"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Lägg till en wiki-sida"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Modifiera wiki-sida"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Ta bort wiki-sida"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Lägg till wiki-länk"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Modifiera wiki-link"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Ta bort wiki-länk"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Mofifiera projekt"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Lägg till medlem"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Ta bort medlem"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Ta bort projekt"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Lägg till medlem"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Ta bort medlem"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Administrera projektvärden"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Administratorroller"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "ägare"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Felaktiga argument"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Felaktigt bildformat"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Inget giltigt mallnamn"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Inte giltigt mallbeskrivning"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Du har inte behörighet att se det. "
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Delvisa uppdateringar stöds inte. "
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "Projekt-ID stämmer inte mellan objekt och projekt"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "projekt"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "innehållstyp"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "objekt-ID"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "ändrad datum"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "bifogad fil"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "undviks"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "sortera"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "Dyker upp i "
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Anpassa"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Text"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Text med flera rader"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Datum"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "typ"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "värden"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "Användarhistorie"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "uppgift"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "Ärende"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Existerar redan med samma namn. "
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "status"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "titel"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "färg"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "Tilldelad till"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "är ett beställarkrav"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "är ett krav från arbetsgruppen"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Kommentaren är redan borttagit. "
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Kommentaren är inte borttagit"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Ändra"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Skapa"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Ta bort"
@@ -1449,7 +1501,7 @@ msgstr "borttaget"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Ej tilldelad"
@@ -1496,95 +1548,75 @@ msgstr "Från:"
msgid "To:"
msgstr "Till:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "innehåll"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "blockerad notering"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Du har inte behörighet att sätta sprinten till det här ärendet."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Du har inte behörighet att sätta status till det här ärendet. "
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "Du har inte behörighet att sätta allvarsgrad till det här ärendet. "
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "Du har inte behörighet att sätta prioriteten för det här ärendet. "
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Du har inte behörighet att lägga till typen till ärendet. "
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "status"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "Allvarsgrad"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "prioritet"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "milstolpe"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "färdig datum"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "titel"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "Tilldelad till"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "extern referens"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Gillar"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Gillar"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "slugg"
@@ -1596,8 +1628,9 @@ msgstr "Beräknad startdatum"
msgid "estimated finish date"
msgstr "Beräknad slutdato"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "är stängd"
@@ -1609,290 +1642,384 @@ msgstr "disponerar"
msgid "The estimated start must be previous to the estimated finish."
msgstr "Beräknad startdatum måste vara tidigare än beräknad slutdatum. "
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Det finns ingen sprint med det här ID-numret"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "är blockerad"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' parameter är obligatoriskt"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project' parameter är obligatoriskt"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "e-post"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "skapa som"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "textsträng"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "Invitation - extra text"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "användarorder"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "Användaren är redan medlem i projekt"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "standardpoäng"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "standard US-poäng"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "standardpoäng"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "standard status för uppgift"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "standard prioritet"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "standard allvarsgrad"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "standard status för ärende"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "standard typ för ärende"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr ""
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "medlemmar"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "totalt antal milstolpar"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "totalt antal historiepoäng"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "aktivt panel för inkorg"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "aktiv kanban-panel"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "aktiv wiki-panel"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "aktiv panel för ärenden"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "videokonferensssystem"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "videokonferens - extra data"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "mall skapas"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "anonyma rättigheter"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "användarbehörigheter"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "är privat"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "anonyma rättigheter"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "användarbehörigheter"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr ""
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr ""
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "färger för taggar"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "uppdaterad dato och tid"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "räkna"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr ""
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr ""
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr ""
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr ""
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr ""
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "konfigurera moduler"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "är arkiverad"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "färg"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "begränsad arbete pågår"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "värde"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "ägarens standardroll"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "standard val"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "US statuser"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "poäng"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "statuser för uppgifter"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "status för ärenden"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "ärendentyper"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "prioriteter"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "allvarsgrad"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "roller"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Involverad"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Alla"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Ingen"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "skapad dato och tid"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "historienotat"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "notifiera användare"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "Visad"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Notifiering finns för användaren och projektet"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Felaktigt värde för notifieringen"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2364,159 +2491,179 @@ msgid ""
"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n"
msgstr ""
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "Listan på bevakare består av felaktiga användare"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "Versionen måste vara ett heltal"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "Versionsparametern är ogiltig"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Versionen stämmer inte med den aktuella versionen"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "version"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "E-postadressen är redan använd"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Fel roll for projektet"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Standardval"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Status för användarhistorien"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Poäng"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Status för uppgifter"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Status för ärenden"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Ärendetyper"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Prioritet"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Allvarsgrad"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Roller"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Framtidig sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Projektslut"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Textsträngen är ogiltig"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "taggar"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "färger för taggar"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "Du har inte behörighet åt att sätta sprinten till en uppgift"
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr "Du har inte behörighet att sätta använderhistorien till en uppgift."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "Du har inte behörighet att sätta status till en uppgift. "
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "sortera US"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "Sortera uppgiftstavlan"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "är Iocaine"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Det är ingen uppgift med det ID-numret"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -2857,12 +3004,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -2878,12 +3025,12 @@ msgstr ""
"man lär sig om produkten, funktioner och kunder. "
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -2897,306 +3044,391 @@ msgstr ""
"uppdragskön."
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Ny"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Leveransklar"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "Pågående"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Klart till test"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Färdig"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Arkiverad"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Stängd"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Behöver information"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Uppskjutit"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Avslått"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Bugg"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Fråga"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "Förbättring"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Låg"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Hög"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "Önskelista"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "Mindre"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Viktig"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Kritiskt"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Design"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Framsida"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Baksida"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Produktägare"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Intressent"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr ""
"Du har inte behörighet för att lägga sprinten till den här användarhistorien"
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr ""
"Du har inte behörighet till att sätta den här statusen till "
"användarhistorien."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "Skapar användarhistorie #{ref} - {subject}"
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "roll"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "sortera inkorgen"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "sortera sprintar"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "färdig datum"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "är ett beställarkrav"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "är ett krav från arbetsgruppen"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "skapad från ärende"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Det är inga användarhistoria med det ID-numret"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Det är inga projekt med det ID-numret"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Det är inga användarhistoria-status med det ID-numret"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "E-postadressen är redan använd"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Det är inga uppgifter med det ID-numret"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Fel roll for projektet"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Standardval"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Status för användarhistorien"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Poäng"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Status för uppgifter"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Status för ärenden"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Ärendetyper"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Prioritet"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Allvarsgrad"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Roller"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Röster"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Rösta"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'content' parametern är obligatoriskt"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parametern är obligatoriskt"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "senastste ändring"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "Kolla historie API för exakt skillnad"
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3224,54 +3456,54 @@ msgstr ""
msgid "Important dates"
msgstr "Viktiga datum"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "E-post-dublett"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Ingen giltig e-postadress"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Ogiltigt användarnamn eller e-postadress"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "E-posten skickades korrekt"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "Parameter för nuvarande lösenord krävs"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Parameter för nytt lösenord krävs"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Felaktig längd på lösenord. Minst 6 alfanumeriska tecken krävs."
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "Fel lösenord"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Fel. Är du säker på att strängen är korrekt och att du inte har använt det "
"tidigare?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Fel, är du säker på att textsträngen är korrekt? "
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "status för administratorn"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -3279,25 +3511,25 @@ msgstr ""
"Anger om användaren har alla behörigheter utan att uttryckligen tilldela "
"dem. "
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "användarnamn"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Obligatoriskt. 30 eller färre alfanumeriska tecken, bokstäver och /./-/_ . "
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Skriv in ett giltigt användarnamn"
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "aktiv"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -3305,71 +3537,63 @@ msgstr ""
"Anger om användaren ska betraktas som aktiv. Avmarkera detta i stället för "
"att ta bort kontot."
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biografi"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "foto"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "blev medlem datum"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "standardspråk"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "standardtema"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "standard tidzon"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "farglägg taggar"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "e-poststräng"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "ny e-postadress"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "behörigheter"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "felaktigt"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Användarnamn eller lösenord passar inte."
@@ -3490,47 +3714,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Du har blivit Taiganiserad!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Det är inga roller med det ID-numret"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "felaktigt"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr "Dublett-nyckelvärden bryter unik begränsning. Key \"{}\" finns redan."
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "nyckel"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "Länk"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "hemlig nyckel"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "statuskod"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "begär data"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "begär titel"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "responsdata"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "responstitel"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "varaktighet"
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index 15ea255e..d070f778 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/LC_MESSAGES/django.po
@@ -10,8 +10,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@@ -21,159 +21,163 @@ msgstr ""
"Language: tr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr ""
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "geçersiz kayıt tipi"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "geçersiz giriş tipi"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "Kullanıcı adı zaten kullanımda."
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "E-posta zaten kullanımda."
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "Kupon geçerli hiç bir davetle uyuşmuyor."
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "Kullanıcı zaten kayıtlı."
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "Bu kullanızı halihazırda zaten projenin bir üyesi."
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi."
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "Geçersiz kupon"
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "geçersiz kullanıcı adı"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
"Zorunlu. 255 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "Kullanıcı adı zaten kullanımda."
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "E-posta zaten kullanımda."
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "Kupon geçerli hiç bir davetle uyuşmuyor."
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "Kullanıcı zaten kayıtlı."
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "Bu kullanızı halihazırda zaten projenin bir üyesi."
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi."
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "Geçersiz kupon"
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "Bu alan zorunlu."
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "Geçersiz değer."
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "%s' değeri ya Doğru ya da Yanlış olmalıdır."
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
"Harfler, rakamlar, altçizgi ve kesme işaretinden oluşan geçerli bir 'satır' "
"girin."
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Geçerli bir seçenek belirleyin. %(value)s değeri mevcut seçenekler arasında "
"yok."
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "Geçerli bir e-posta adresi girin."
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "Tarih biçemi yanlış. Belirtilen biçemlerden birini kullanın: %s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "Tarih saat biçemi yanlış. Belirtilen biçemlerden birini kullanın: %s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "Zaman biçemi yanlış. Belirtilen biçemlerden birini kullanın: %s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "Bir tam sayı girin."
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr ""
"Bu değerin %(limit_value)s değerine eşit ya da daha az olduğundan emin olun."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr ""
"Bu değerin %(limit_value)s değerine eşit ya da daha fazla olduğundan emin "
"olun."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" değeri kesirli bir sayı olmalıdır."
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "Bir sayın girin."
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Toplamda %s basamaktan fazla olmadığından emin olun."
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "%s ondalık değerinden fazla olmalıdığından emin olun."
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr ""
"Virgülden önceki rakamların %s basamaktan fazla olmadığından emin olun."
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Dosya ibraz edilmedi. Formdan kodlama tipini kontrol edin."
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "Dosya ibraz edilmedi."
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "İbraz edilen dosya boş"
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -181,13 +185,13 @@ msgstr ""
"Bu dosya adının en fazla %(max)d karakterden oluştuğundan (uzunluğunun "
"%(length)d olduğundan) emin olun"
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr ""
"Lütfen bir dosya ibraz edin ya da onay kutusunu seçmeyin, ikisini birden "
"olmaz."
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -195,180 +199,177 @@ msgstr ""
"Geçerli bir resim yükleyin. Yüklenen dosya ya bozulmuş bir resim ya da bir "
"resim dosyası değil."
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Engellenmiş nesne"
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "Sayfa 'last'(son) değil, tamsayıya da çevrilemiyor."
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "Geçersiz sayfa (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "Geçersiz izin tanımı."
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "Geçersiz pk '%s' - nesne mevcut değil."
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "Hatalı tip. Beklenen pk değeri, alınan %s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr "%s=%s objesi mevcut değil."
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "Geçersiz hiperlink - URL eşleşmesi yok"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "Geçersiz hiperlink - Doğru olmayan URL eşleşmesi"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "Yapılandırma hatasından dolayı geçersiz hiperlink"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "Geçersiz hiperlink - nesne mevcut değil."
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "Hatalı tip. Beklenen url dizges, alınan %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "Geçersiz veri"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "Girdi sağlanmadı"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr "Yeni bir madde oluşturlamıyor, sadece var olanlar güncellenebilir."
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "Bir madde listesi bekleniyor."
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "Bulunamadı"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "İzin verilmedi"
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "Sunucu uygulaması hatası"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "Bağlantı hatası."
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "Bozulmuş talep."
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "Hatalı oturum açma bilgileri."
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "Oturum açma bilgileri girilmedi."
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "Bu eylemi gerçekleştirebilmek için gerekli izne sahip değilsiniz."
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "'%s' yöntemine izin verilmiyor."
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr ""
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "'%s' talebinde desteklenmeyen ortam tipi mevcut"
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr ""
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr ""
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "Belirlenmeyen hata"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "Bulunamadı."
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr ""
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "Hatalı parametreler."
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "Veri doğrulama hatası"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "Hatalı ya da geçersiz parametreler için Bütünlük Hatası "
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "Ön şart hatası"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr "Daha fazla proje için yer kalmadı."
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "Parametre tipleri filtresinde hata."
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "'project' değeri numerik olmalı."
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "etiketler"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -423,7 +424,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -435,22 +436,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-"Taiga Destek:\n"
-"%(support_url)s\n"
-"
\n"
-"Bize ulaşın:\n"
-"\n"
-"%(support_email)s\n"
-"\n"
-"
\n"
-"E-posta listesi:\n"
-"\n"
-"%(mailing_list_url)s\n"
-""
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -501,103 +486,88 @@ msgstr ""
"\n"
"Yorumlar: %(comment)s"
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "En azından bir role ihtiyacımız var"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "İhtiyaç duyulan döküm dosyası"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "Geçersiz döküm biçemi"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" bu projede bulunamadı"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "Geçersiz içerik. {\"key\": \"value\",...} şeklinde olması zorunlu"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "Geçersiz özel alanlar içeriyor."
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "Aynı isimde proje bulunmakta"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "İçeri aktarılan proje verisinde hata"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "İçeri aktarılan rollerde hata"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "İçeri aktarılan üyeliklerde hata"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "özel öznitelikler içeri aktarılırken hata"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "İçeri aktarılan sprintlerde hata"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "İçeri aktarılan kullanıcı hikayelerinde hata"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "İçeri aktarılan görevlerde hata"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "İçeri aktarılan taleplerde hata"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "İçeri aktarılan kullanıcı hikayelerinde hata"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "İçeri aktarılan görevlerde hata"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "İçeri aktarılan wiki sayfalarında hata"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "İçeri aktarılan wiki bağlantılarında hata"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "İçeri aktarılan etiketlerde hata"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "zaman çizelgesi içeri aktarılırken hata"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "Proje dökümü oluşturulurken hata"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -617,15 +587,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "Proje dökümü yükleniyorken hata"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -861,77 +831,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] Projenizin döküm dosyası içe aktarıldı"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" bu projede bulunamadı"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "Geçersiz içerik. {\"key\": \"value\",...} şeklinde olması zorunlu"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "Geçersiz özel alanlar içeriyor."
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "Aynı isimde proje bulunmakta"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "Kimlik doğrulama gerekli"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "isim"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "İkon url"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "web"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "tanı"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "Sonraki url"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr ""
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "kullanıcı"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "uygulama"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "tam ad"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "e-posta adresi"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "yorum"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "oluşturma tarihi"
@@ -960,7 +950,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "Ekstra bilgi"
@@ -994,513 +984,577 @@ msgstr ""
"\n"
"[Taiga] %(full_name)s <%(email)s> den geri bildirim\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr ""
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "Proje mevcut değil."
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "Kötü imza"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "Referans gösterilmiş varlık mevcut değil"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "Durum mevcut değil"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "Bitbucket commiti ile durum değişti"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "Geçersiz talep bilgisi"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "Bitbucket ten oluşturulan talep"
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
+"\n"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "Geçersiz talep yorum bilgisi"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "Geçersiz talep bilgisi"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"Bitbucket yorum:\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "Githup commit i ile durum değişt."
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "GitHub dan oluşturulan talep"
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"GitHub dan gelen Yorum:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "Referans gösterilmiş varlık mevcut değil"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr ""
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "Durum mevcut değil"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "GitLab dan oluşturuldu"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"Gitlabdan gelen yorum:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "Projeyi gör"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "Aşamaları gör"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "Kullanıcı hikayelerini gör"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "Görevleri gör"
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "Talepleri gör"
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "Wiki sayfalarını gör"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "Wiki bağlantılarını gör"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "Üyelik talep et"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "Projeye kullanıcı hikayesi ekle"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "Kullanıcı hikayelerine yorumlar ekle"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "Görevlere yorumlar ekle"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "Talepler ekle"
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "Taleplere yorumlar ekle"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "Wiki sayfası ekle"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "Wiki sayfası düzenle"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "Wiki bağlantısı ekle"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "Wiki bağlantısı düzenle"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "Aşama ekle"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "Aşama düzenle"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "Aşama sil"
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "Kullanıcı hikayesini gör"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "Kullanıcı hikayesi ekle"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "Kullanıcı hikayesi düzenle"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "Kullanıcı hikayesi sil"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "Görev ekle"
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "Görev düzenle"
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "Görev sil"
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "Talep ekle"
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "Talep düzenle"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "Talep sil"
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "Wiki sayfası ekle"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "Wiki sayfası düzenle"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "Wiki sayfası sil"
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "Wiki bağlantısı ekle"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "Wiki bağlantısı düzenle"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "Wiki bağlantısı sil"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "Proje düzenle"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "Üye ekle"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "Üye sil"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "Proje sil"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "Üye ekle"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "Üye sil"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "Admin proje değerleri"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "Yönetici rolleri"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "sahip"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "Eksik parametreq"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "Geçersiz resim biçemi"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "Geçersiz şablon adı"
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "Geçersiz şablon tanımı"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr "Geçersiz kullanıcı id"
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr "Kullanıcı mevcut değil"
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr "Kullanıcı zaten proje üyesi durumunda"
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "Görebilmek için yetkiniz yok."
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "Kısmi güncellemeler desteklenmiyor"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "proje"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "içerik tipi"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "nesne id"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "düzenleme tarihi"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "eklenmiş dosya"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "kaldırıldı"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "sıra"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "Özel"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr "Yetkili kalmadığı için proje bloklandı"
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "Metin"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "Çoklu-satır metin"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "Tarih"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr "Url"
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "tip"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "değerler"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "kullanıcı hikayesi"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "görev"
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "talep"
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "Aynı isimler bir tane daha mevcut."
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "durum"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "konu"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "renk"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "atanmış"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "istemci gereksinimi"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "takım gereksinimi"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "Yorum zaten silinmiş"
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "Yorum silinmedi"
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "Değiştir"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "Oluştur"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "Sil"
@@ -1556,7 +1610,7 @@ msgstr "silindi"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "Atanmamış"
@@ -1603,95 +1657,75 @@ msgstr "Kimden:"
msgid "To:"
msgstr "Kime:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "içerik"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "engellenmiş not"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "sprint"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "Bu talep için bu sprinti ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "Bu talep için bu durumu ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "Bu talep için bu kritiklik derecesini ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "Bu talep için bu öncelik durumunu ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "Bu talep için bu tipi ayarlamaya yetkiniz yok."
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "durum"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "önem derecesi"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "öncelik"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "aşama"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "bitirme tarihi"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "konu"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "atanmış"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "dış referans"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "Beğen"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "Beğeniler"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "satır"
@@ -1703,8 +1737,9 @@ msgstr "yaklaşık başlama tarihi"
msgid "estimated finish date"
msgstr "yaklaşık bitiş tarihi"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "kapatılmış"
@@ -1716,290 +1751,384 @@ msgstr "taşınabilirlik"
msgid "The estimated start must be previous to the estimated finish."
msgstr "Tahmini başlangıç, tahmini bitişten önce olmalı"
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "Bu id ye sahip sprint yok"
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "engellenmiş"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' parametresi zorunlu"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'proje' parametresi zorunlu"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "e-posta"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr ""
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "kupon"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "Davetiye ekstra metni"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "kullanıcı sırası"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "Kullanıcı zaten projenin üyesi"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "varsayılan puanlar"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "varsayılan KH durumu"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "varsayılan puanlar"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "varsayılan görev durumu"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "varsayılan öncelik"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "varsayılan önem derecesi"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "varsayılan talep durumu"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "varsayılan talep tipi"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "logo"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "üyeler"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "aşamaların toplamı"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "toplam hikaye puanı"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "aktif birikmiş iler paneli"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "aktif kanban paneli"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "aktif wiki paneli"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "aktif talep paneli"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "video konferans sistemi"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "videokonferans ekstra verisi"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "oluşturma şablonu"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "anonim izinler"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "kullanıcı izinleri"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "gizli"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "anonim izinler"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "kullanıcı izinleri"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr "vitrinde"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr "insan arıyor"
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "etiket renkleri"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr "engellenmiş kod"
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "yükleme tarih-saati"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "sayı"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr "geçen hafta fanları"
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr "geçen ayın fanları"
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr "geçen yılın fanları"
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr "geçen haftanın aktiviteleri"
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr "geçen ayın aktiviteleri"
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr "geçen yılın aktiviteleri"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "modül ayarları"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "arşivlenmiş"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "renk"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr ""
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "değer"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "varsayılan sahip rolü"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "varsayılan ayarlar"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "kh durumları"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "puanlar"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "görev durumları"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "talep durumları"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "talep tipleri"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "öncelikler"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "önem durumları"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "roller"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "Müdahil"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "Hepsi"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "Hiçbiri"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "oluşturma tarih-saati"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "tarihçe girdileri"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "kullanıcıları bilgilendir"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "İzlenen"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "Belirtilen kullanıcı ve proje için bilgilendirme mevcut"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "Bildirim düzeyi için geçersiz değer"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2537,159 +2666,179 @@ msgstr ""
"\n"
"[%(project)s] Silinmiş Wiki Sayfası \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "İzleyiciler arasında geçersiz kullanıcılar var"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "Sürüm rakamsal bir şey olmalıdır"
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "Sürüm parametresi geçersiz"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "Sürüm geçerli olanla uyuşmuyor"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "sürüm"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "E-posta adresi önceden alınmış"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "Proje için geçersiz rol"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "Varsayılan ayarlar"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "Kullanıcı hikayelerinin durumları"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "Puanlar"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "Görevlerin durumları"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "Taleplerin durumları"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "Taleplerin tipleri"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "Öncelikler"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "Önem dereceleri"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "Roller"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "Gelecek sprint"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "Proje Sonu"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "Kupon geçersiz"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "etiketler"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "etiket renkleri"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "Bu görev için sprint ayarlamanız için izniniz yok."
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr "Bu görev için kullanıcı hikayesi ayarlama izniniz yok."
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "Bu görev için bu durumu ayarlama izniniz yok."
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "kh sırası"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "görev panosu sırası"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "baldıran zehri"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "Bu id ile ilgili bir görev yok"
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3042,12 +3191,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3058,12 +3207,12 @@ msgid ""
msgstr ""
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3072,303 +3221,388 @@ msgid ""
msgstr ""
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "Yeni"
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "Hazır"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "Devam ediyor"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "Teste hazır"
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "Bitmiş"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "Arşivlenmiş"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "Kapatılmış"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "Bilgi İhtiyacı"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "Ertelenmiş"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "Reddedilmiş"
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "Hata"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "Soru"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "İyileştirme"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "Düşük"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "Normal"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "Yüksek"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "İstek Listesi"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr ""
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "Önemli"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "Kritik"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "UX"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "Tasarım"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "Ön"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "Arka"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "Ürün Sahibi"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "Paydaş"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr "Bu kullanıcı hikayesine bu sprinti ayarlama izniniz yok."
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr "Bu kullanıcı hikayesine bu durumu ayarlama yetkiniz yok."
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr ""
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "rol"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "birikmiş işler sırası"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "sprint sırası"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "bitiş tarihi"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "istemci gereksinimi"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "takım gereksinimi"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "talepden oluştur"
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "Bu id ye sahip kullanıcı hikayesi yok"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "Bu id ye sahip proje yok"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "Bu id ye sahip kullanıcı hikayesi durumu yok"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "E-posta adresi önceden alınmış"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "Bu id ye sahip görev durumu yok"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "Proje için geçersiz rol"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "Varsayılan ayarlar"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "Kullanıcı hikayelerinin durumları"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "Puanlar"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "Görevlerin durumları"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "Taleplerin durumları"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "Taleplerin tipleri"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "Öncelikler"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "Önem dereceleri"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "Roller"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "Oylar"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "Oy"
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'content' parametresi zorunlu"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id' parametresi zorunlu"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "son düzenleyen"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3396,148 +3630,140 @@ msgstr ""
msgid "Important dates"
msgstr "Önemli tarihler"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr ""
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "Geçersiz e-posta"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "Geçersiz kullanıcı adı ya da e-posta"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "Posta başarıyla gönderildi!"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr ""
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "Yeni parola parametresi gerekli"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "Geçersiz parola uzunluğu, en az 6 karaktere ihtiyaç var"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr ""
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr ""
"Geçersiz geçerli bir kupona sahip olduğunuzdan ve bu kuponu daha önce "
"kullanmadığınızdan emin misiniz?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "Geçersiz, kuponun doğru olduğuna emin misin?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "superuser durumu"
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "kullanıcı adı"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr ""
"Zorunlu. 30 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "Geçerli bir kullanıcı adı girin."
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "aktif"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "biyografi"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "fotoğraf"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "katılma tarihi"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "varsayılan dil"
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "varsayılan tema"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "varsayılan saat dilimi"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "etiketleri renklendir"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "e-posta kuponu"
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "yeni e-posta adresi"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "izinler"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "Geçersiz"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin."
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "Kullanıcı adı veya parola kullanıcıyla uyuşmuyor"
@@ -3660,47 +3886,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "Taigalandınız!"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "Bu id ye sahip yok yok"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "Geçersiz"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin."
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr ""
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "anahtar"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "gizli anahtar"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "durum kodu"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "talep verisi"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "talep başlıkları"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "cevap verisi"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "cevap başlıkları"
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "süre"
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index 5537e07f..fcbeb9ea 100644
--- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po
+++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
@@ -11,8 +11,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-05-01 19:09+0200\n"
-"PO-Revision-Date: 2016-05-01 17:09+0000\n"
+"POT-Creation-Date: 2016-09-28 10:29+0200\n"
+"PO-Revision-Date: 2016-09-20 10:50+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hant/)\n"
@@ -22,339 +22,340 @@ msgstr ""
"Language: zh-Hant\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-#: taiga/auth/api.py:100
+#: taiga/auth/api.py:102
msgid "Public register is disabled."
msgstr "註冊功能暫不開放"
-#: taiga/auth/api.py:133
+#: taiga/auth/api.py:135
msgid "invalid register type"
msgstr "無效的註冊類型"
-#: taiga/auth/api.py:146
+#: taiga/auth/api.py:148
msgid "invalid login type"
msgstr "無效的登入類型"
-#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
+#: taiga/auth/services.py:76
+msgid "Username is already in use."
+msgstr "本用戶名稱已被註冊"
+
+#: taiga/auth/services.py:79
+msgid "Email is already in use."
+msgstr "本電子郵件已使用"
+
+#: taiga/auth/services.py:95
+msgid "Token not matches any valid invitation."
+msgstr "代碼與任何有效的邀請不相符"
+
+#: taiga/auth/services.py:123
+msgid "User is already registered."
+msgstr "使用者已被註冊。"
+
+#: taiga/auth/services.py:147
+msgid "This user is already a member of the project."
+msgstr "使用者已是專案成員"
+
+#: taiga/auth/services.py:173
+msgid "Error on creating new user."
+msgstr "無法創建新使用者"
+
+#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56
+#: taiga/external_apps/services.py:36 taiga/projects/api.py:364
+#: taiga/projects/api.py:385
+msgid "Invalid token"
+msgstr "無效的代碼 "
+
+#: taiga/auth/validators.py:37 taiga/users/validators.py:44
msgid "invalid username"
msgstr "無效使用者名稱"
-#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
+#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "必填。最多255字元(可為數字,字母,符號....)"
-#: taiga/auth/services.py:75
-msgid "Username is already in use."
-msgstr "本用戶名稱已被註冊"
-
-#: taiga/auth/services.py:78
-msgid "Email is already in use."
-msgstr "本電子郵件已使用"
-
-#: taiga/auth/services.py:94
-msgid "Token not matches any valid invitation."
-msgstr "代碼與任何有效的邀請不相符"
-
-#: taiga/auth/services.py:122
-msgid "User is already registered."
-msgstr "使用者已被註冊。"
-
-#: taiga/auth/services.py:146
-msgid "This user is already a member of the project."
-msgstr "使用者已是專案成員"
-
-#: taiga/auth/services.py:172
-msgid "Error on creating new user."
-msgstr "無法創建新使用者"
-
-#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55
-#: taiga/external_apps/services.py:35 taiga/projects/api.py:376
-#: taiga/projects/api.py:397
-msgid "Invalid token"
-msgstr "無效的代碼 "
-
-#: taiga/base/api/fields.py:292
+#: taiga/base/api/fields.py:294
msgid "This field is required."
msgstr "此欄位是必要的。"
-#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335
+#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337
msgid "Invalid value."
msgstr "無效的數值"
-#: taiga/base/api/fields.py:477
+#: taiga/base/api/fields.py:479
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' 數值必須為「是」或「否」。"
-#: taiga/base/api/fields.py:541
+#: taiga/base/api/fields.py:543
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr "輸入有效的代稱,其包括字母,數字,底底線與連字符號"
-#: taiga/base/api/fields.py:556
+#: taiga/base/api/fields.py:558
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr "請做個有效的選擇。 %(value)s 並不是可以選的選項。"
-#: taiga/base/api/fields.py:619
+#: taiga/base/api/fields.py:621
+msgid "You email domain is not allowed"
+msgstr ""
+
+#: taiga/base/api/fields.py:630
msgid "Enter a valid email address."
msgstr "輸入無效之電子郵件地址"
-#: taiga/base/api/fields.py:661
+#: taiga/base/api/fields.py:672
#, python-format
msgid "Date has wrong format. Use one of these formats instead: %s"
msgstr "資料格式錯誤,請改用這些格式取代:%s"
-#: taiga/base/api/fields.py:725
+#: taiga/base/api/fields.py:736
#, python-format
msgid "Datetime has wrong format. Use one of these formats instead: %s"
msgstr "日期格式錯誤,請使用這些格式取代:%s"
-#: taiga/base/api/fields.py:795
+#: taiga/base/api/fields.py:806
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
msgstr "時間格式錯誤,請使用這些格式取代:%s"
-#: taiga/base/api/fields.py:852
+#: taiga/base/api/fields.py:863
msgid "Enter a whole number."
msgstr "輸入一個整數"
-#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
+#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
msgstr "確認此值小於等於 %(limit_value)s."
-#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907
+#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
msgstr "確認此值大於等於 %(limit_value)s."
-#: taiga/base/api/fields.py:884
+#: taiga/base/api/fields.py:895
#, python-format
msgid "\"%s\" value must be a float."
msgstr "\"%s\" 數值必須為一個浮點數"
-#: taiga/base/api/fields.py:905
+#: taiga/base/api/fields.py:916
msgid "Enter a number."
msgstr "輸入一組號碼"
-#: taiga/base/api/fields.py:908
+#: taiga/base/api/fields.py:919
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "確認全部沒有多於 %s位數 "
-#: taiga/base/api/fields.py:909
+#: taiga/base/api/fields.py:920
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "確認沒有多於 %s十進位數 "
-#: taiga/base/api/fields.py:910
+#: taiga/base/api/fields.py:921
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "確認在小數點前沒有多於 %s位數 "
-#: taiga/base/api/fields.py:977
+#: taiga/base/api/fields.py:988
msgid "No file was submitted. Check the encoding type on the form."
msgstr "無檔案送出,請 確認表格中的編碼 格式"
-#: taiga/base/api/fields.py:978
+#: taiga/base/api/fields.py:989
msgid "No file was submitted."
msgstr "無檔案送出"
-#: taiga/base/api/fields.py:979
+#: taiga/base/api/fields.py:990
msgid "The submitted file is empty."
msgstr "送出的檔案無內容"
-#: taiga/base/api/fields.py:980
+#: taiga/base/api/fields.py:991
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
msgstr "確認檔案名稱最多有 %(max)d 字元 (它有 %(length)d)."
-#: taiga/base/api/fields.py:981
+#: taiga/base/api/fields.py:992
msgid "Please either submit a file or check the clear checkbox, not both."
msgstr "請上傳擋案或是勾選清除方格中二選一"
-#: taiga/base/api/fields.py:1021
+#: taiga/base/api/fields.py:1032
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
-#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:642
-#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58
-#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174
-#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238
-#: taiga/webhooks/api.py:68
+#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211
+#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671
+#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292
+#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59
+#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287
+#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392
+#: taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
-#: taiga/base/api/pagination.py:213
+#: taiga/base/api/pagination.py:214
msgid "Page is not 'last', nor can it be converted to an int."
msgstr "頁數不是最後,或者它無法轉成整數 "
-#: taiga/base/api/pagination.py:217
+#: taiga/base/api/pagination.py:218
#, python-format
msgid "Invalid page (%(page_number)s): %(message)s"
msgstr "無效頁面I (%(page_number)s): %(message)s"
-#: taiga/base/api/permissions.py:64
+#: taiga/base/api/permissions.py:66
msgid "Invalid permission definition."
msgstr "無效的權限定義 "
-#: taiga/base/api/relations.py:245
+#: taiga/base/api/relations.py:247
#, python-format
msgid "Invalid pk '%s' - object does not exist."
msgstr "無效的pk '%s'- 物件並不存在"
-#: taiga/base/api/relations.py:246
+#: taiga/base/api/relations.py:248
#, python-format
msgid "Incorrect type. Expected pk value, received %s."
msgstr "不正確類型,預期為pk值,收到%s."
-#: taiga/base/api/relations.py:334
+#: taiga/base/api/relations.py:336
#, python-format
msgid "Object with %s=%s does not exist."
msgstr " 包含%s=%s物件不存在"
-#: taiga/base/api/relations.py:370
+#: taiga/base/api/relations.py:372
msgid "Invalid hyperlink - No URL match"
msgstr "無效的超鏈接 - 無相符之網址"
-#: taiga/base/api/relations.py:371
+#: taiga/base/api/relations.py:373
msgid "Invalid hyperlink - Incorrect URL match"
msgstr "無效的超鏈接 - 不正確的相符網址"
-#: taiga/base/api/relations.py:372
+#: taiga/base/api/relations.py:374
msgid "Invalid hyperlink due to configuration error"
msgstr "因設定出錯的無效超鏈接"
-#: taiga/base/api/relations.py:373
+#: taiga/base/api/relations.py:375
msgid "Invalid hyperlink - object does not exist."
msgstr "無效的超鏈接 - 物件並不存在"
-#: taiga/base/api/relations.py:374
+#: taiga/base/api/relations.py:376
#, python-format
msgid "Incorrect type. Expected url string, received %s."
msgstr "不正確類型,預期為網址格式,收到的是 %s."
-#: taiga/base/api/serializers.py:320
+#: taiga/base/api/serializers.py:324
msgid "Invalid data"
msgstr "無效的資料"
-#: taiga/base/api/serializers.py:412
+#: taiga/base/api/serializers.py:416
msgid "No input provided"
msgstr "無輸入提供"
-#: taiga/base/api/serializers.py:575
+#: taiga/base/api/serializers.py:579
msgid "Cannot create a new item, only existing items may be updated."
msgstr "無法建立新項目,只能更新現有項目"
-#: taiga/base/api/serializers.py:586
+#: taiga/base/api/serializers.py:590
msgid "Expected a list of items."
msgstr "期待的項目清單"
-#: taiga/base/api/views.py:125
+#: taiga/base/api/views.py:126
msgid "Not found"
msgstr "找不到"
-#: taiga/base/api/views.py:128
+#: taiga/base/api/views.py:129
msgid "Permission denied"
msgstr "許可遭拒絕 "
-#: taiga/base/api/views.py:476
+#: taiga/base/api/views.py:477
msgid "Server application error"
msgstr "伺服器應用出錯"
-#: taiga/base/connectors/exceptions.py:25
+#: taiga/base/connectors/exceptions.py:26
msgid "Connection error."
msgstr "連結出錯"
-#: taiga/base/exceptions.py:77
+#: taiga/base/exceptions.py:79
msgid "Malformed request."
msgstr "遭封鎖"
-#: taiga/base/exceptions.py:82
+#: taiga/base/exceptions.py:84
msgid "Incorrect authentication credentials."
msgstr "不正確的授權認證 "
-#: taiga/base/exceptions.py:87
+#: taiga/base/exceptions.py:89
msgid "Authentication credentials were not provided."
msgstr "未担供授權認證 "
-#: taiga/base/exceptions.py:92
+#: taiga/base/exceptions.py:94
msgid "You do not have permission to perform this action."
msgstr "你無權限進行此動作"
-#: taiga/base/exceptions.py:97
+#: taiga/base/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr "不允許 '%s' 方式"
-#: taiga/base/exceptions.py:105
+#: taiga/base/exceptions.py:107
msgid "Could not satisfy the request's Accept header"
msgstr "無法滿藙要求其接受標頭 "
-#: taiga/base/exceptions.py:114
+#: taiga/base/exceptions.py:116
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr "不支援的資料類型'%s' 被提出"
-#: taiga/base/exceptions.py:122
+#: taiga/base/exceptions.py:124
msgid "Request was throttled."
msgstr "要求無法執行 "
-#: taiga/base/exceptions.py:123
+#: taiga/base/exceptions.py:125
#, python-format
msgid "Expected available in %d second%s."
msgstr "預期在 %d 秒%s.內可取得 "
-#: taiga/base/exceptions.py:137
+#: taiga/base/exceptions.py:139
msgid "Unexpected error"
msgstr "無預期的錯誤"
-#: taiga/base/exceptions.py:149
+#: taiga/base/exceptions.py:151
msgid "Not found."
msgstr "找不到"
-#: taiga/base/exceptions.py:154
+#: taiga/base/exceptions.py:156
msgid "Method not supported for this endpoint."
msgstr "從GitHub取得原始碼"
-#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170
+#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172
msgid "Wrong arguments."
msgstr "錯誤的參數"
-#: taiga/base/exceptions.py:174
+#: taiga/base/exceptions.py:176
msgid "Data validation error"
msgstr "資料有效性錯誤"
-#: taiga/base/exceptions.py:186
+#: taiga/base/exceptions.py:188
msgid "Integrity Error for wrong or invalid arguments"
msgstr "因錯誤或無效參數,一致性出錯"
-#: taiga/base/exceptions.py:193
+#: taiga/base/exceptions.py:195
msgid "Precondition error"
msgstr "前提出錯"
-#: taiga/base/exceptions.py:217
+#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
-#: taiga/base/filters.py:79 taiga/base/filters.py:444
+#: taiga/base/filters.py:81 taiga/base/filters.py:462
msgid "Error in filter params types."
msgstr "過濾參數類型出錯"
-#: taiga/base/filters.py:133 taiga/base/filters.py:232
-#: taiga/projects/filters.py:63
+#: taiga/base/filters.py:135 taiga/base/filters.py:242
+#: taiga/projects/filters.py:64
msgid "'project' must be an integer value."
msgstr "專案須為整數值"
-#: taiga/base/tags.py:26
-msgid "tags"
-msgstr "標籤"
-
#: taiga/base/templates/emails/base-body-html.jinja:6
msgid "Taiga"
msgstr "Taiga"
@@ -409,7 +410,7 @@ msgid ""
" Contact us:"
"strong>\n"
" \n"
+"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n"
" %(support_email)s\n"
" \n"
"
\n"
@@ -421,33 +422,6 @@ msgid ""
" \n"
" "
msgstr ""
-"\n"
-"Taiga 支援:\n"
-"\n"
-""
-"%(support_url)s\n"
-"\n"
-"
\n"
-"\n"
-"聯絡我們:\n"
-"\n"
-"\n"
-"\n"
-"%(support_email)s\n"
-"\n"
-"\n"
-"\n"
-"
\n"
-"\n"
-"郵件群組:\n"
-"\n"
-"\n"
-"\n"
-"%(mailing_list_url)s\n"
-"\n"
-""
#: taiga/base/templates/emails/hero-body-html.jinja:6
msgid "You have been Taigatized"
@@ -498,103 +472,88 @@ msgstr ""
"\n"
"評論: %(comment)s"
-#: taiga/export_import/api.py:119
+#: taiga/export_import/api.py:127
msgid "We needed at least one role"
msgstr "我們至少需要一個角色"
-#: taiga/export_import/api.py:309
+#: taiga/export_import/api.py:323
msgid "Needed dump file"
msgstr "需要的堆存檔案"
-#: taiga/export_import/api.py:316
+#: taiga/export_import/api.py:333
msgid "Invalid dump format"
msgstr "無效堆存格式"
-#: taiga/export_import/serializers.py:178
-msgid "{}=\"{}\" not found in this project"
-msgstr "{}=\"{}\" 無法在此專案中找到"
-
-#: taiga/export_import/serializers.py:443
-#: taiga/projects/custom_attributes/serializers.py:104
-msgid "Invalid content. It must be {\"key\": \"value\",...}"
-msgstr "無效內容。必須為 {\"key\": \"value\",...}"
-
-#: taiga/export_import/serializers.py:458
-#: taiga/projects/custom_attributes/serializers.py:119
-msgid "It contain invalid custom fields."
-msgstr "包括無效慣例欄位"
-
-#: taiga/export_import/serializers.py:528
-#: taiga/projects/mixins/serializers.py:38
-msgid "Name duplicated for the project"
-msgstr "專案的名稱被複製了"
-
-#: taiga/export_import/services/store.py:621
-#: taiga/export_import/services/store.py:639
+#: taiga/export_import/services/store.py:718
+#: taiga/export_import/services/store.py:736
msgid "error importing project data"
msgstr "滙入重要專案資料出錯"
-#: taiga/export_import/services/store.py:646
+#: taiga/export_import/services/store.py:743
msgid "error importing roles"
msgstr "滙入角色出錯"
-#: taiga/export_import/services/store.py:651
+#: taiga/export_import/services/store.py:748
msgid "error importing memberships"
msgstr "滙入成員資格出錯"
-#: taiga/export_import/services/store.py:661
+#: taiga/export_import/services/store.py:759
msgid "error importing lists of project attributes"
msgstr "滙入標籤出錯"
-#: taiga/export_import/services/store.py:665
+#: taiga/export_import/services/store.py:763
msgid "error importing default project attributes values"
msgstr "滙入預設專案屬性數值出錯"
-#: taiga/export_import/services/store.py:674
+#: taiga/export_import/services/store.py:774
msgid "error importing custom attributes"
msgstr "滙入客制性屬出錯"
-#: taiga/export_import/services/store.py:679
+#: taiga/export_import/services/store.py:778
msgid "error importing sprints"
msgstr "滙入衝刺任務出錯"
-#: taiga/export_import/services/store.py:683
-msgid "error importing user stories"
-msgstr "滙入使用者故事出錯"
-
-#: taiga/export_import/services/store.py:687
-msgid "error importing tasks"
-msgstr "滙入任務出錯"
-
-#: taiga/export_import/services/store.py:691
+#: taiga/export_import/services/store.py:782
msgid "error importing issues"
msgstr "滙入問題出錯"
-#: taiga/export_import/services/store.py:695
+#: taiga/export_import/services/store.py:786
+msgid "error importing user stories"
+msgstr "滙入使用者故事出錯"
+
+#: taiga/export_import/services/store.py:790
+msgid "error importing epics"
+msgstr ""
+
+#: taiga/export_import/services/store.py:794
+msgid "error importing tasks"
+msgstr "滙入任務出錯"
+
+#: taiga/export_import/services/store.py:798
msgid "error importing wiki pages"
msgstr "滙入維基頁出錯"
-#: taiga/export_import/services/store.py:699
+#: taiga/export_import/services/store.py:802
msgid "error importing wiki links"
msgstr "滙入維基連結出錯"
-#: taiga/export_import/services/store.py:703
+#: taiga/export_import/services/store.py:806
msgid "error importing tags"
msgstr "滙入標籤出錯"
-#: taiga/export_import/services/store.py:707
+#: taiga/export_import/services/store.py:810
msgid "error importing timelines"
msgstr "滙入時間軸出錯"
-#: taiga/export_import/services/store.py:731
+#: taiga/export_import/services/store.py:832
msgid "unexpected error importing project"
msgstr ""
-#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57
+#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
msgstr "產生專案傾倒時出錯"
-#: taiga/export_import/tasks.py:81
+#: taiga/export_import/tasks.py:91
#, python-brace-format
msgid ""
"\n"
@@ -614,15 +573,15 @@ msgid ""
"------------"
msgstr ""
-#: taiga/export_import/tasks.py:110
+#: taiga/export_import/tasks.py:120
msgid "Error loading project dump"
msgstr "載入專案傾倒時出錯"
-#: taiga/export_import/tasks.py:111
+#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
-#: taiga/export_import/tasks.py:125
+#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
@@ -858,77 +817,97 @@ msgstr ""
msgid "[%(project)s] Your project dump has been imported"
msgstr "[%(project)s] 您堆存的專案已滙入"
-#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67
-#: taiga/external_apps/api.py:74
+#: taiga/export_import/validators/fields.py:144
+msgid "{}=\"{}\" not found in this project"
+msgstr "{}=\"{}\" 無法在此專案中找到"
+
+#: taiga/export_import/validators/validators.py:150
+#: taiga/projects/custom_attributes/validators.py:109
+msgid "Invalid content. It must be {\"key\": \"value\",...}"
+msgstr "無效內容。必須為 {\"key\": \"value\",...}"
+
+#: taiga/export_import/validators/validators.py:165
+#: taiga/projects/custom_attributes/validators.py:124
+msgid "It contain invalid custom fields."
+msgstr "包括無效慣例欄位"
+
+#: taiga/export_import/validators/validators.py:245
+#: taiga/projects/validators.py:52
+msgid "Name duplicated for the project"
+msgstr "專案的名稱被複製了"
+
+#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
+#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr "要求取得授權"
-#: taiga/external_apps/models.py:34
-#: taiga/projects/custom_attributes/models.py:35
-#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:478 taiga/projects/models.py:517
-#: taiga/projects/models.py:542 taiga/projects/models.py:579
-#: taiga/projects/models.py:602 taiga/projects/models.py:625
-#: taiga/projects/models.py:660 taiga/projects/models.py:683
-#: taiga/users/admin.py:53 taiga/users/models.py:292
-#: taiga/webhooks/models.py:28
+#: taiga/external_apps/models.py:35
+#: taiga/projects/custom_attributes/models.py:36
+#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145
+#: taiga/projects/models.py:512 taiga/projects/models.py:545
+#: taiga/projects/models.py:581 taiga/projects/models.py:603
+#: taiga/projects/models.py:637 taiga/projects/models.py:657
+#: taiga/projects/models.py:677 taiga/projects/models.py:709
+#: taiga/projects/models.py:729 taiga/users/admin.py:54
+#: taiga/users/models.py:292 taiga/webhooks/models.py:29
msgid "name"
msgstr "姓名"
-#: taiga/external_apps/models.py:36
+#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr "網址圖標"
-#: taiga/external_apps/models.py:37
+#: taiga/external_apps/models.py:38
msgid "web"
msgstr "網頁"
-#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60
-#: taiga/projects/custom_attributes/models.py:36
-#: taiga/projects/history/templatetags/functions.py:24
-#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150
-#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
-#: taiga/projects/userstories/models.py:92
+#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61
+#: taiga/projects/custom_attributes/models.py:37
+#: taiga/projects/epics/models.py:55
+#: taiga/projects/history/templatetags/functions.py:25
+#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149
+#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62
+#: taiga/projects/userstories/models.py:95
msgid "description"
msgstr "描述"
-#: taiga/external_apps/models.py:40
+#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr "下一個網址"
-#: taiga/external_apps/models.py:42
+#: taiga/external_apps/models.py:43
msgid "secret key for ciphering the application tokens"
msgstr "應用程式的密碼字符數列"
-#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30
-#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51
+#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31
+#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52
msgid "user"
msgstr "使用者"
-#: taiga/external_apps/models.py:60
+#: taiga/external_apps/models.py:61
msgid "application"
msgstr "應用程式"
-#: taiga/feedback/models.py:24 taiga/users/models.py:138
+#: taiga/feedback/models.py:25 taiga/users/models.py:137
msgid "full name"
msgstr "全名"
-#: taiga/feedback/models.py:26 taiga/users/models.py:133
+#: taiga/feedback/models.py:27 taiga/users/models.py:132
msgid "email address"
msgstr "電子郵件"
-#: taiga/feedback/models.py:28
+#: taiga/feedback/models.py:29
msgid "comment"
msgstr "評論"
-#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47
-#: taiga/projects/custom_attributes/models.py:45
-#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32
-#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157
-#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88
-#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84
-#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40
-#: taiga/userstorage/models.py:28
+#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
+#: taiga/projects/custom_attributes/models.py:46
+#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52
+#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49
+#: taiga/projects/models.py:156 taiga/projects/models.py:737
+#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
+#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54
+#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29
msgid "created date"
msgstr "創建日期"
@@ -957,7 +936,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:120
+#: taiga/projects/admin.py:106 taiga/users/admin.py:120
msgid "Extra info"
msgstr "額外資訊"
@@ -990,543 +969,577 @@ msgstr ""
"\n"
"[Taiga] 回饋來自 %(full_name)s <%(email)s>\n"
-#: taiga/hooks/api.py:53
+#: taiga/hooks/api.py:54
msgid "The payload is not a valid json"
msgstr "載荷為無效json"
-#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139
-#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111
+#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152
+#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200
+#: taiga/projects/userstories/api.py:273
msgid "The project doesn't exist"
msgstr "專案不存在"
-#: taiga/hooks/api.py:65
+#: taiga/hooks/api.py:66
msgid "Bad signature"
msgstr "錯誤簽名"
-#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76
-#: taiga/hooks/gitlab/event_hooks.py:74
-msgid "The referenced element doesn't exist"
-msgstr "參考元素不存在"
-
-#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83
-#: taiga/hooks/gitlab/event_hooks.py:81
-msgid "The status doesn't exist"
-msgstr "狀態不存在"
-
-#: taiga/hooks/bitbucket/event_hooks.py:95
-msgid "Status changed from BitBucket commit"
-msgstr "來自BitBucket 投入的狀態更新"
-
-#: taiga/hooks/bitbucket/event_hooks.py:124
-#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114
-msgid "Invalid issue information"
-msgstr "無效的問題資訊"
-
-#: taiga/hooks/bitbucket/event_hooks.py:140
+#: taiga/hooks/event_hooks.py:66
#, python-brace-format
msgid ""
-"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in "
+"[{platform}#{number}]({comment_url} \"Go to comment\"):\n"
"\n"
-"{description}"
+"\"{comment_message}\""
msgstr ""
-"來自[@{bitbucket_user_name}]({bitbucket_user_url} 的問題\"詳見 "
-"@{bitbucket_user_name}'s BitBucket profile\") BitBucket.\n"
-"源自BitBucket 問題: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\"):\n"
+
+#: taiga/hooks/event_hooks.py:71
+#, python-brace-format
+msgid ""
+"Comment From {platform}:\n"
"\n"
-"{description}"
+"> {comment_message}"
+msgstr ""
-#: taiga/hooks/bitbucket/event_hooks.py:151
-msgid "Issue created from BitBucket."
-msgstr "來自BitBucket的問題:"
-
-#: taiga/hooks/bitbucket/event_hooks.py:175
-#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193
-#: taiga/hooks/gitlab/event_hooks.py:153
+#: taiga/hooks/event_hooks.py:84
msgid "Invalid issue comment information"
msgstr "無效的議題評論資訊"
-#: taiga/hooks/bitbucket/event_hooks.py:183
+#: taiga/hooks/event_hooks.py:103
#, python-brace-format
msgid ""
-"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} "
+"profile\") from [{platform}#{number}]({url} \"Go to issue\")."
msgstr ""
-" [@{bitbucket_user_name}]({bitbucket_user_url}之評論 \"參見 "
-"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n"
-"源自BitBucket 問題: [bb#{number} - {subject}]({bitbucket_url} \"Go to "
-"'bb#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/bitbucket/event_hooks.py:194
+#: taiga/hooks/event_hooks.py:107
+#, python-brace-format
+msgid "Issue created from {platform}."
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:120
+msgid "Invalid issue information"
+msgstr "無效的問題資訊"
+
+#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171
+msgid "unknown user"
+msgstr ""
+
+#: taiga/hooks/event_hooks.py:156
#, python-brace-format
msgid ""
-"Comment From BitBucket:\n"
+"{user_text} changed the status from [{platform} commit]({commit_url} \"See "
+"commit '{commit_id} - {commit_message}'\")\n"
"\n"
-"{message}"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"來自BitBucket的評論:\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:97
+#: taiga/hooks/event_hooks.py:161
#, python-brace-format
msgid ""
-"Status changed by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
+"Changed status from {platform} commit.\n"
+"\n"
+" - Status: **{src_status}** → **{dst_status}**"
msgstr ""
-"來自 [@{github_user_name}]({github_user_url}之狀態變更 \"參見"
-"@{github_user_name}'s GitHub profile\") 來自GitHub之投入 [{commit_id}]"
-"({commit_url} \"See commit '{commit_id} - {commit_message}'\")."
-#: taiga/hooks/github/event_hooks.py:108
-msgid "Status changed from GitHub commit."
-msgstr "來自GitHub投入的狀態更新"
-
-#: taiga/hooks/github/event_hooks.py:158
+#: taiga/hooks/event_hooks.py:179
#, python-brace-format
msgid ""
-"Issue created by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\"):\n"
-"\n"
-"{description}"
+"This {type_name} has been mentioned by {user_text} in the [{platform} commit]"
+"({commit_url} \"See commit '{commit_id} - {commit_message}'\") "
+"\"{commit_message}\""
msgstr ""
-"來自 [@{github_user_name}]({github_user_url}提出的問題 \"參見"
-"@{github_user_name}'s GitHub profile\") 來自GitHub. Github上原始問題 : "
-"[gh#{number} - {subject}]({github_url} ”跳至 'gh#{number} - {subject}'\") \n"
-"{description}"
-#: taiga/hooks/github/event_hooks.py:169
-msgid "Issue created from GitHub."
-msgstr "自來GitHub 的問題 "
-
-#: taiga/hooks/github/event_hooks.py:201
+#: taiga/hooks/event_hooks.py:184
#, python-brace-format
msgid ""
-"Comment by [@{github_user_name}]({github_user_url} \"See "
-"@{github_user_name}'s GitHub profile\") from GitHub.\n"
-"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to "
-"'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
+"This issue has been mentioned in the {platform} commit \"{commit_message}\""
msgstr ""
-"來自 [@{github_user_name}]({github_user_url}之評論 \"參見"
-"@{github_user_name}'s GitHub profile\") 來自GitHub. Gibhub上原始問題 : "
-"[gh#{number} - {subject}]({github_url} ”跳至 'gh#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-#: taiga/hooks/github/event_hooks.py:212
-#, python-brace-format
-msgid ""
-"Comment From GitHub:\n"
-"\n"
-"{message}"
-msgstr ""
-"來自 GitHub:\n"
-"\n"
-"{message}"
+#: taiga/hooks/event_hooks.py:206
+msgid "The referenced element doesn't exist"
+msgstr "參考元素不存在"
-#: taiga/hooks/gitlab/event_hooks.py:87
-msgid "Status changed from GitLab commit"
-msgstr "來自GitLab提供的狀態變更"
+#: taiga/hooks/event_hooks.py:222
+msgid "The status doesn't exist"
+msgstr "狀態不存在"
-#: taiga/hooks/gitlab/event_hooks.py:129
-msgid "Created from GitLab"
-msgstr "創建立GitLab"
-
-#: taiga/hooks/gitlab/event_hooks.py:161
-#, python-brace-format
-msgid ""
-"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See "
-"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n"
-"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-msgstr ""
-" [@{gitlab_user_name}]({gitlab_user_url}之評論 \"參見 @{gitlab_user_name}'s "
-"GitLab profile\") from GitLab.\n"
-"源自 GitLab 問題: [gl#{number} - {subject}]({gitlab_url} \"Go to "
-"'gl#{number} - {subject}'\")\n"
-"\n"
-"{message}"
-
-#: taiga/hooks/gitlab/event_hooks.py:172
-#, python-brace-format
-msgid ""
-"Comment From GitLab:\n"
-"\n"
-"{message}"
-msgstr ""
-"來自GitLab的評論:\n"
-"\n"
-"{message}"
-
-#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32
-#: taiga/permissions/permissions.py:52
+#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project"
msgstr "檢視專案"
-#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33
-#: taiga/permissions/permissions.py:54
+#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36
msgid "View milestones"
msgstr "檢視里程碑"
-#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34
+#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41
+msgid "View epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:26
msgid "View user stories"
msgstr "檢視使用者故事"
-#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36
-#: taiga/permissions/permissions.py:64
+#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53
msgid "View tasks"
msgstr "檢視任務 "
-#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35
-#: taiga/permissions/permissions.py:69
+#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59
msgid "View issues"
msgstr "檢視問題 "
-#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37
-#: taiga/permissions/permissions.py:74
+#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65
msgid "View wiki pages"
msgstr "檢視維基頁"
-#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38
-#: taiga/permissions/permissions.py:79
+#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71
msgid "View wiki links"
msgstr "檢視維基連結"
-#: taiga/permissions/permissions.py:39
-msgid "Request membership"
-msgstr "要求加入會員"
-
-#: taiga/permissions/permissions.py:40
-msgid "Add user story to project"
-msgstr "專案中新增使用者故事"
-
-#: taiga/permissions/permissions.py:41
-msgid "Add comments to user stories"
-msgstr "使用者故事附加評論"
-
-#: taiga/permissions/permissions.py:42
-msgid "Add comments to tasks"
-msgstr "任務附加評論"
-
-#: taiga/permissions/permissions.py:43
-msgid "Add issues"
-msgstr "加入問題 "
-
-#: taiga/permissions/permissions.py:44
-msgid "Add comments to issues"
-msgstr "問題加入評論"
-
-#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75
-msgid "Add wiki page"
-msgstr "新增維基頁"
-
-#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76
-msgid "Modify wiki page"
-msgstr "修改維基頁"
-
-#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80
-msgid "Add wiki link"
-msgstr "新增維基連結"
-
-#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81
-msgid "Modify wiki link"
-msgstr "修改維基連結"
-
-#: taiga/permissions/permissions.py:55
+#: taiga/permissions/choices.py:37
msgid "Add milestone"
msgstr "加入里程碑"
-#: taiga/permissions/permissions.py:56
+#: taiga/permissions/choices.py:38
msgid "Modify milestone"
msgstr "修改里程碑"
-#: taiga/permissions/permissions.py:57
+#: taiga/permissions/choices.py:39
msgid "Delete milestone"
msgstr "刪除里程碑 "
-#: taiga/permissions/permissions.py:59
+#: taiga/permissions/choices.py:42
+msgid "Add epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:43
+msgid "Modify epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:44
+msgid "Comment epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:45
+msgid "Delete epic"
+msgstr ""
+
+#: taiga/permissions/choices.py:47
msgid "View user story"
msgstr "檢視使用者故事"
-#: taiga/permissions/permissions.py:60
+#: taiga/permissions/choices.py:48
msgid "Add user story"
msgstr "新增使用者故事"
-#: taiga/permissions/permissions.py:61
+#: taiga/permissions/choices.py:49
msgid "Modify user story"
msgstr "修改使用者故事"
-#: taiga/permissions/permissions.py:62
+#: taiga/permissions/choices.py:50
+msgid "Comment user story"
+msgstr ""
+
+#: taiga/permissions/choices.py:51
msgid "Delete user story"
msgstr "刪除使用者故事"
-#: taiga/permissions/permissions.py:65
+#: taiga/permissions/choices.py:54
msgid "Add task"
msgstr "新增任務 "
-#: taiga/permissions/permissions.py:66
+#: taiga/permissions/choices.py:55
msgid "Modify task"
msgstr "修改任務 "
-#: taiga/permissions/permissions.py:67
+#: taiga/permissions/choices.py:56
+msgid "Comment task"
+msgstr ""
+
+#: taiga/permissions/choices.py:57
msgid "Delete task"
msgstr "刪除任務 "
-#: taiga/permissions/permissions.py:70
+#: taiga/permissions/choices.py:60
msgid "Add issue"
msgstr "新增問題 "
-#: taiga/permissions/permissions.py:71
+#: taiga/permissions/choices.py:61
msgid "Modify issue"
msgstr "修改問題"
-#: taiga/permissions/permissions.py:72
+#: taiga/permissions/choices.py:62
+msgid "Comment issue"
+msgstr ""
+
+#: taiga/permissions/choices.py:63
msgid "Delete issue"
msgstr "刪除問題 "
-#: taiga/permissions/permissions.py:77
+#: taiga/permissions/choices.py:66
+msgid "Add wiki page"
+msgstr "新增維基頁"
+
+#: taiga/permissions/choices.py:67
+msgid "Modify wiki page"
+msgstr "修改維基頁"
+
+#: taiga/permissions/choices.py:68
+msgid "Comment wiki page"
+msgstr ""
+
+#: taiga/permissions/choices.py:69
msgid "Delete wiki page"
msgstr "刪除維基頁 "
-#: taiga/permissions/permissions.py:82
+#: taiga/permissions/choices.py:72
+msgid "Add wiki link"
+msgstr "新增維基連結"
+
+#: taiga/permissions/choices.py:73
+msgid "Modify wiki link"
+msgstr "修改維基連結"
+
+#: taiga/permissions/choices.py:74
msgid "Delete wiki link"
msgstr "刪除維基連結"
-#: taiga/permissions/permissions.py:86
+#: taiga/permissions/choices.py:78
msgid "Modify project"
msgstr "修改專案"
-#: taiga/permissions/permissions.py:87
-msgid "Add member"
-msgstr "新增成員"
-
-#: taiga/permissions/permissions.py:88
-msgid "Remove member"
-msgstr "移除成員"
-
-#: taiga/permissions/permissions.py:89
+#: taiga/permissions/choices.py:79
msgid "Delete project"
msgstr "刪除專案"
-#: taiga/permissions/permissions.py:90
+#: taiga/permissions/choices.py:80
+msgid "Add member"
+msgstr "新增成員"
+
+#: taiga/permissions/choices.py:81
+msgid "Remove member"
+msgstr "移除成員"
+
+#: taiga/permissions/choices.py:82
msgid "Admin project values"
msgstr "管理員專案數值"
-#: taiga/permissions/permissions.py:91
+#: taiga/permissions/choices.py:83
msgid "Admin roles"
msgstr "管理員角色"
-#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38
-#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43
-#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61
-#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66
-#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69
-#: taiga/userstorage/models.py:26
+#: taiga/projects/admin.py:100
+msgid "Privacity"
+msgstr ""
+
+#: taiga/projects/admin.py:112
+msgid "Modules"
+msgstr ""
+
+#: taiga/projects/admin.py:120
+msgid "Default values"
+msgstr ""
+
+#: taiga/projects/admin.py:126
+msgid "Activity"
+msgstr ""
+
+#: taiga/projects/admin.py:131
+msgid "Fans"
+msgstr ""
+
+#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39
+#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37
+#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161
+#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
+#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
+#: taiga/users/admin.py:69 taiga/userstorage/models.py:27
msgid "owner"
msgstr "所有者"
-#: taiga/projects/api.py:165 taiga/users/api.py:220
+#: taiga/projects/admin.py:200
+#, python-brace-format
+msgid "{count} successfully made public."
+msgstr ""
+
+#: taiga/projects/admin.py:201
+msgid "Make public"
+msgstr ""
+
+#: taiga/projects/admin.py:215
+#, python-brace-format
+msgid "{count} successfully made private."
+msgstr ""
+
+#: taiga/projects/admin.py:216
+msgid "Make private"
+msgstr ""
+
+#: taiga/projects/admin.py:246
+#, python-format
+msgid "Delete selected %(verbose_name_plural)s"
+msgstr ""
+
+#: taiga/projects/api.py:150 taiga/users/api.py:237
msgid "Incomplete arguments"
msgstr "不完整參數"
-#: taiga/projects/api.py:169 taiga/users/api.py:225
+#: taiga/projects/api.py:154 taiga/users/api.py:242
msgid "Invalid image format"
msgstr "無效的圖片檔案"
-#: taiga/projects/api.py:230
+#: taiga/projects/api.py:215
msgid "Not valid template name"
msgstr "非有效樣板名稱 "
-#: taiga/projects/api.py:233
+#: taiga/projects/api.py:218
msgid "Not valid template description"
msgstr "無效樣板描述"
-#: taiga/projects/api.py:356
+#: taiga/projects/api.py:344
msgid "Invalid user id"
msgstr ""
-#: taiga/projects/api.py:362
+#: taiga/projects/api.py:350
msgid "The user doesn't exist"
msgstr ""
-#: taiga/projects/api.py:366
+#: taiga/projects/api.py:354
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:672
+#: taiga/projects/api.py:701
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:706
+#: taiga/projects/api.py:735
msgid "You don't have permisions to see that."
msgstr "您無觀看權限"
-#: taiga/projects/attachments/api.py:51
+#: taiga/projects/attachments/api.py:54
msgid "Partial updates are not supported"
msgstr "不支援部份更新"
-#: taiga/projects/attachments/api.py:66
+#: taiga/projects/attachments/api.py:69
+msgid "Object id issue isn't exists"
+msgstr ""
+
+#: taiga/projects/attachments/api.py:72
msgid "Project ID not matches between object and project"
msgstr "專案ID不符合物件與專案"
-#: taiga/projects/attachments/models.py:40
-#: taiga/projects/custom_attributes/models.py:42
-#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45
-#: taiga/projects/models.py:466 taiga/projects/models.py:492
-#: taiga/projects/models.py:523 taiga/projects/models.py:552
-#: taiga/projects/models.py:585 taiga/projects/models.py:608
-#: taiga/projects/models.py:635 taiga/projects/models.py:666
-#: taiga/projects/notifications/models.py:73
-#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42
-#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30
-#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
+#: taiga/projects/attachments/models.py:41
+#: taiga/projects/custom_attributes/models.py:43
+#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50
+#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500
+#: taiga/projects/models.py:522 taiga/projects/models.py:559
+#: taiga/projects/models.py:587 taiga/projects/models.py:613
+#: taiga/projects/models.py:643 taiga/projects/models.py:663
+#: taiga/projects/models.py:687 taiga/projects/models.py:715
+#: taiga/projects/notifications/models.py:74
+#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43
+#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34
+#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303
msgid "project"
msgstr "專案"
-#: taiga/projects/attachments/models.py:42
+#: taiga/projects/attachments/models.py:43
msgid "content type"
msgstr "內容類型"
-#: taiga/projects/attachments/models.py:44
+#: taiga/projects/attachments/models.py:45
msgid "object id"
msgstr "物件ID"
-#: taiga/projects/attachments/models.py:50
-#: taiga/projects/custom_attributes/models.py:47
-#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52
-#: taiga/projects/models.py:160 taiga/projects/models.py:692
-#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87
-#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30
+#: taiga/projects/attachments/models.py:51
+#: taiga/projects/custom_attributes/models.py:48
+#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55
+#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159
+#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51
+#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
+#: taiga/userstorage/models.py:31
msgid "modified date"
msgstr "修改日期"
-#: taiga/projects/attachments/models.py:55
+#: taiga/projects/attachments/models.py:56
msgid "attached file"
msgstr "附加檔案"
-#: taiga/projects/attachments/models.py:57
+#: taiga/projects/attachments/models.py:58
msgid "sha1"
msgstr "sha1"
-#: taiga/projects/attachments/models.py:59
+#: taiga/projects/attachments/models.py:60
msgid "is deprecated"
msgstr "棄用"
-#: taiga/projects/attachments/models.py:61
-#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482
-#: taiga/projects/models.py:519 taiga/projects/models.py:546
-#: taiga/projects/models.py:581 taiga/projects/models.py:604
-#: taiga/projects/models.py:629 taiga/projects/models.py:662
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
+#: taiga/projects/attachments/models.py:62
+#: taiga/projects/custom_attributes/models.py:41
+#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58
+#: taiga/projects/models.py:516 taiga/projects/models.py:549
+#: taiga/projects/models.py:583 taiga/projects/models.py:607
+#: taiga/projects/models.py:639 taiga/projects/models.py:659
+#: taiga/projects/models.py:681 taiga/projects/models.py:711
+#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298
msgid "order"
msgstr "次序"
-#: taiga/projects/choices.py:22
+#: taiga/projects/choices.py:23
msgid "AppearIn"
msgstr "AppearIn"
-#: taiga/projects/choices.py:23
+#: taiga/projects/choices.py:24
msgid "Jitsi"
msgstr "Jitsi"
-#: taiga/projects/choices.py:24
+#: taiga/projects/choices.py:25
msgid "Custom"
msgstr "自定"
-#: taiga/projects/choices.py:25
+#: taiga/projects/choices.py:26
msgid "Talky"
msgstr "Talky"
-#: taiga/projects/choices.py:32
+#: taiga/projects/choices.py:35
msgid "This project is blocked due to payment failure"
msgstr ""
-#: taiga/projects/choices.py:33
+#: taiga/projects/choices.py:36
msgid "This project is blocked by admin staff"
msgstr ""
-#: taiga/projects/choices.py:34
+#: taiga/projects/choices.py:37
msgid "This project is blocked because the owner left"
msgstr ""
-#: taiga/projects/custom_attributes/choices.py:27
+#: taiga/projects/choices.py:38
+msgid "This project is blocked while it's deleted"
+msgstr ""
+
+#: taiga/projects/custom_attributes/choices.py:28
msgid "Text"
msgstr "單行文字"
-#: taiga/projects/custom_attributes/choices.py:28
+#: taiga/projects/custom_attributes/choices.py:29
msgid "Multi-Line Text"
msgstr "多行列文字"
-#: taiga/projects/custom_attributes/choices.py:29
+#: taiga/projects/custom_attributes/choices.py:30
msgid "Date"
msgstr "日期"
-#: taiga/projects/custom_attributes/choices.py:30
+#: taiga/projects/custom_attributes/choices.py:31
msgid "Url"
msgstr ""
-#: taiga/projects/custom_attributes/models.py:39
-#: taiga/projects/issues/models.py:47
+#: taiga/projects/custom_attributes/models.py:40
+#: taiga/projects/issues/models.py:45
msgid "type"
msgstr "類型"
-#: taiga/projects/custom_attributes/models.py:88
+#: taiga/projects/custom_attributes/models.py:95
msgid "values"
msgstr "價值"
-#: taiga/projects/custom_attributes/models.py:98
-#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36
+#: taiga/projects/custom_attributes/models.py:105
+msgid "epic"
+msgstr ""
+
+#: taiga/projects/custom_attributes/models.py:121
+#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38
msgid "user story"
msgstr "使用者故事"
-#: taiga/projects/custom_attributes/models.py:113
+#: taiga/projects/custom_attributes/models.py:137
msgid "task"
msgstr "任務 "
-#: taiga/projects/custom_attributes/models.py:128
+#: taiga/projects/custom_attributes/models.py:153
msgid "issue"
msgstr "問題 "
-#: taiga/projects/custom_attributes/serializers.py:58
+#: taiga/projects/custom_attributes/validators.py:58
msgid "Already exists one with the same name."
msgstr "已存在相同姓名"
-#: taiga/projects/history/api.py:71
+#: taiga/projects/epics/api.py:92
+msgid "You don't have permissions to set this status to this epic."
+msgstr ""
+
+#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35
+#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
+msgid "ref"
+msgstr "ref"
+
+#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39
+#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
+msgid "status"
+msgstr "狀態"
+
+#: taiga/projects/epics/models.py:45
+msgid "epics order"
+msgstr ""
+
+#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59
+#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
+msgid "subject"
+msgstr "主旨"
+
+#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520
+#: taiga/projects/models.py:555 taiga/projects/models.py:611
+#: taiga/projects/models.py:641 taiga/projects/models.py:661
+#: taiga/projects/models.py:685 taiga/projects/models.py:713
+#: taiga/users/models.py:139
+msgid "color"
+msgstr "顏色"
+
+#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63
+#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
+msgid "assigned to"
+msgstr "指派給"
+
+#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100
+msgid "is client requirement"
+msgstr "客戶要求"
+
+#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102
+msgid "is team requirement"
+msgstr "團隊要求"
+
+#: taiga/projects/epics/models.py:69
+msgid "user stories"
+msgstr ""
+
+#: taiga/projects/epics/validators.py:37
+msgid "There's no epic with that id"
+msgstr ""
+
+#: taiga/projects/history/api.py:93
+msgid "comment is required"
+msgstr ""
+
+#: taiga/projects/history/api.py:96
+msgid "deleted comments can't be edited"
+msgstr ""
+
+#: taiga/projects/history/api.py:130
msgid "Comment already deleted"
msgstr "評論已刪除 "
-#: taiga/projects/history/api.py:90
+#: taiga/projects/history/api.py:151
msgid "Comment not deleted"
msgstr "不可刪除 之評論 "
-#: taiga/projects/history/choices.py:27
+#: taiga/projects/history/choices.py:31
msgid "Change"
msgstr "更改"
-#: taiga/projects/history/choices.py:28
+#: taiga/projects/history/choices.py:32
msgid "Create"
msgstr "創建"
-#: taiga/projects/history/choices.py:29
+#: taiga/projects/history/choices.py:33
msgid "Delete"
msgstr "刪除 "
@@ -1582,7 +1595,7 @@ msgstr "移除 "
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146
-#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55
+#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56
msgid "Unassigned"
msgstr "無指定"
@@ -1629,95 +1642,75 @@ msgstr "來自:"
msgid "To:"
msgstr "給:"
-#: taiga/projects/history/templatetags/functions.py:25
-#: taiga/projects/wiki/models.py:34
+#: taiga/projects/history/templatetags/functions.py:26
+#: taiga/projects/wiki/models.py:38
msgid "content"
msgstr "內容"
-#: taiga/projects/history/templatetags/functions.py:26
-#: taiga/projects/mixins/blocked.py:32
+#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/mixins/blocked.py:33
msgid "blocked note"
msgstr "封鎖筆記"
-#: taiga/projects/history/templatetags/functions.py:27
+#: taiga/projects/history/templatetags/functions.py:28
msgid "sprint"
msgstr "衝刺任務"
-#: taiga/projects/issues/api.py:158
+#: taiga/projects/issues/api.py:156
msgid "You don't have permissions to set this sprint to this issue."
msgstr "您無權限設定此問題的衝刺任務"
-#: taiga/projects/issues/api.py:162
+#: taiga/projects/issues/api.py:160
msgid "You don't have permissions to set this status to this issue."
msgstr "您無權限設定此問題的狀態"
-#: taiga/projects/issues/api.py:166
+#: taiga/projects/issues/api.py:164
msgid "You don't have permissions to set this severity to this issue."
msgstr "您無權限設定此問題的嚴重性"
-#: taiga/projects/issues/api.py:170
+#: taiga/projects/issues/api.py:168
msgid "You don't have permissions to set this priority to this issue."
msgstr "您無權限設定此問題的優先性"
-#: taiga/projects/issues/api.py:174
+#: taiga/projects/issues/api.py:172
msgid "You don't have permissions to set this type to this issue."
msgstr "您無權限設定此問題的類型"
-#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36
-#: taiga/projects/userstories/models.py:59
-msgid "ref"
-msgstr "ref"
-
-#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40
-#: taiga/projects/userstories/models.py:69
-msgid "status"
-msgstr "狀態"
-
-#: taiga/projects/issues/models.py:43
+#: taiga/projects/issues/models.py:41
msgid "severity"
msgstr "嚴重性"
-#: taiga/projects/issues/models.py:45
+#: taiga/projects/issues/models.py:43
msgid "priority"
msgstr "優先性"
-#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45
-#: taiga/projects/userstories/models.py:62
+#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46
+#: taiga/projects/userstories/models.py:65
msgid "milestone"
msgstr "里程碑"
-#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52
+#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53
msgid "finished date"
msgstr "完成日期"
-#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54
-#: taiga/projects/userstories/models.py:91
-msgid "subject"
-msgstr "主旨"
-
-#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64
-#: taiga/projects/userstories/models.py:95
-msgid "assigned to"
-msgstr "指派給"
-
-#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68
-#: taiga/projects/userstories/models.py:105
+#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
+#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr "外部參考"
-#: taiga/projects/likes/models.py:35
+#: taiga/projects/likes/models.py:36
msgid "Like"
msgstr "喜歡"
-#: taiga/projects/likes/models.py:36
+#: taiga/projects/likes/models.py:37
msgid "Likes"
msgstr "喜歡"
-#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:480 taiga/projects/models.py:544
-#: taiga/projects/models.py:627 taiga/projects/models.py:685
-#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57
-#: taiga/users/models.py:294
+#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147
+#: taiga/projects/models.py:514 taiga/projects/models.py:547
+#: taiga/projects/models.py:605 taiga/projects/models.py:679
+#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36
+#: taiga/users/admin.py:58 taiga/users/models.py:294
msgid "slug"
msgstr "代稱"
@@ -1729,8 +1722,9 @@ msgstr "预計開始日期"
msgid "estimated finish date"
msgstr "預計完成日期"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
-#: taiga/projects/models.py:548 taiga/projects/models.py:631
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518
+#: taiga/projects/models.py:551 taiga/projects/models.py:609
+#: taiga/projects/models.py:683
msgid "is closed"
msgstr "被關閉"
@@ -1742,290 +1736,384 @@ msgstr "disponibility"
msgid "The estimated start must be previous to the estimated finish."
msgstr "預估開始必須在預估結束之前"
-#: taiga/projects/milestones/validators.py:12
-msgid "There's no sprint with that id"
-msgstr "該用戶無衝刺任務 "
+#: taiga/projects/milestones/validators.py:33
+msgid "There's no milestone with that id"
+msgstr ""
-#: taiga/projects/mixins/blocked.py:30
+#: taiga/projects/mixins/blocked.py:31
msgid "is blocked"
msgstr "已封鎖"
-#: taiga/projects/mixins/ordering.py:48
+#: taiga/projects/mixins/ordering.py:49
#, python-brace-format
msgid "'{param}' parameter is mandatory"
msgstr "'{param}' 參數為必要"
-#: taiga/projects/mixins/ordering.py:52
+#: taiga/projects/mixins/ordering.py:53
msgid "'project' parameter is mandatory"
msgstr "'project'參數為必要"
-#: taiga/projects/models.py:78
+#: taiga/projects/models.py:76
msgid "email"
msgstr "電子郵件"
-#: taiga/projects/models.py:80
+#: taiga/projects/models.py:78
msgid "create at"
msgstr "創建於"
-#: taiga/projects/models.py:82 taiga/users/models.py:155
+#: taiga/projects/models.py:80 taiga/users/models.py:154
msgid "token"
msgstr "代號"
-#: taiga/projects/models.py:88
+#: taiga/projects/models.py:86
msgid "invitation extra text"
msgstr "額外文案邀請"
-#: taiga/projects/models.py:91
+#: taiga/projects/models.py:89 taiga/projects/models.py:735
msgid "user order"
msgstr "使用者次序"
-#: taiga/projects/models.py:101
+#: taiga/projects/models.py:105
msgid "The user is already member of the project"
msgstr "使用者已是專案成員"
-#: taiga/projects/models.py:116
-msgid "default points"
-msgstr "預設點數"
+#: taiga/projects/models.py:112
+msgid "default epic status"
+msgstr ""
-#: taiga/projects/models.py:120
+#: taiga/projects/models.py:116
msgid "default US status"
msgstr "預設使用者故事狀態"
-#: taiga/projects/models.py:124
+#: taiga/projects/models.py:119
+msgid "default points"
+msgstr "預設點數"
+
+#: taiga/projects/models.py:123
msgid "default task status"
msgstr "預設任務狀態"
-#: taiga/projects/models.py:127
+#: taiga/projects/models.py:126
msgid "default priority"
msgstr "預設優先性"
-#: taiga/projects/models.py:130
+#: taiga/projects/models.py:129
msgid "default severity"
msgstr "預設嚴重性"
-#: taiga/projects/models.py:134
+#: taiga/projects/models.py:133
msgid "default issue status"
msgstr "預設問題狀態"
-#: taiga/projects/models.py:138
+#: taiga/projects/models.py:137
msgid "default issue type"
msgstr "預設議題類型"
-#: taiga/projects/models.py:154
+#: taiga/projects/models.py:153
msgid "logo"
msgstr "圖標"
-#: taiga/projects/models.py:164
+#: taiga/projects/models.py:163
msgid "members"
msgstr "成員"
-#: taiga/projects/models.py:167
+#: taiga/projects/models.py:166
msgid "total of milestones"
msgstr "全部里程碑"
-#: taiga/projects/models.py:168
+#: taiga/projects/models.py:167
msgid "total story points"
msgstr "全部故事點數"
-#: taiga/projects/models.py:171 taiga/projects/models.py:698
+#: taiga/projects/models.py:170 taiga/projects/models.py:746
+msgid "active epics panel"
+msgstr ""
+
+#: taiga/projects/models.py:172 taiga/projects/models.py:748
msgid "active backlog panel"
msgstr "活躍的待辦任務優先表面板"
-#: taiga/projects/models.py:173 taiga/projects/models.py:700
+#: taiga/projects/models.py:174 taiga/projects/models.py:750
msgid "active kanban panel"
msgstr "活躍的看板式面板"
-#: taiga/projects/models.py:175 taiga/projects/models.py:702
+#: taiga/projects/models.py:176 taiga/projects/models.py:752
msgid "active wiki panel"
msgstr "活躍的維基面板"
-#: taiga/projects/models.py:177 taiga/projects/models.py:704
+#: taiga/projects/models.py:178 taiga/projects/models.py:754
msgid "active issues panel"
msgstr "活躍的問題面板"
-#: taiga/projects/models.py:180 taiga/projects/models.py:707
+#: taiga/projects/models.py:181 taiga/projects/models.py:757
msgid "videoconference system"
msgstr "視訊會議系統"
-#: taiga/projects/models.py:182 taiga/projects/models.py:709
+#: taiga/projects/models.py:183 taiga/projects/models.py:759
msgid "videoconference extra data"
msgstr "視訊會議額外資料"
-#: taiga/projects/models.py:187
+#: taiga/projects/models.py:189
msgid "creation template"
msgstr "創建模版"
-#: taiga/projects/models.py:191
-msgid "anonymous permissions"
-msgstr "匿名權限"
-
-#: taiga/projects/models.py:195
-msgid "user permissions"
-msgstr "使用者權限"
-
-#: taiga/projects/models.py:198 taiga/users/admin.py:61
+#: taiga/projects/models.py:192 taiga/users/admin.py:62
msgid "is private"
msgstr "私密"
-#: taiga/projects/models.py:201
+#: taiga/projects/models.py:194
+msgid "anonymous permissions"
+msgstr "匿名權限"
+
+#: taiga/projects/models.py:196
+msgid "user permissions"
+msgstr "使用者權限"
+
+#: taiga/projects/models.py:199
msgid "is featured"
msgstr " 受矚目的"
-#: taiga/projects/models.py:204
+#: taiga/projects/models.py:202
msgid "is looking for people"
msgstr "正在找人"
-#: taiga/projects/models.py:206
+#: taiga/projects/models.py:204
msgid "loking for people note"
msgstr ""
#: taiga/projects/models.py:218
-msgid "tags colors"
-msgstr "標籤顏色"
-
-#: taiga/projects/models.py:221
msgid "project transfer token"
msgstr ""
-#: taiga/projects/models.py:225
+#: taiga/projects/models.py:222
msgid "blocked code"
msgstr ""
-#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
+#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66
msgid "updated date time"
msgstr "更新日期時間"
-#: taiga/projects/models.py:232 taiga/projects/models.py:244
-#: taiga/projects/votes/models.py:29
+#: taiga/projects/models.py:229 taiga/projects/models.py:241
+#: taiga/projects/votes/models.py:30
msgid "count"
msgstr "數量"
-#: taiga/projects/models.py:235
+#: taiga/projects/models.py:232
msgid "fans last week"
msgstr "上週粉絲"
-#: taiga/projects/models.py:238
+#: taiga/projects/models.py:235
msgid "fans last month"
msgstr "上個月粉絲"
-#: taiga/projects/models.py:241
+#: taiga/projects/models.py:238
msgid "fans last year"
msgstr "去年粉絲"
-#: taiga/projects/models.py:247
+#: taiga/projects/models.py:244
msgid "activity last week"
msgstr "上週活躍成員"
-#: taiga/projects/models.py:250
+#: taiga/projects/models.py:247
msgid "activity last month"
msgstr "上月活躍成員"
-#: taiga/projects/models.py:253
+#: taiga/projects/models.py:250
msgid "activity last year"
msgstr "去年活躍成員"
-#: taiga/projects/models.py:467
+#: taiga/projects/models.py:501
msgid "modules config"
msgstr "模組設定"
-#: taiga/projects/models.py:486
+#: taiga/projects/models.py:553
msgid "is archived"
msgstr "已歸檔"
-#: taiga/projects/models.py:488 taiga/projects/models.py:550
-#: taiga/projects/models.py:583 taiga/projects/models.py:606
-#: taiga/projects/models.py:633 taiga/projects/models.py:664
-#: taiga/users/models.py:140
-msgid "color"
-msgstr "顏色"
-
-#: taiga/projects/models.py:490
+#: taiga/projects/models.py:557
msgid "work in progress limit"
msgstr "工作進度限制"
-#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:585 taiga/userstorage/models.py:33
msgid "value"
msgstr "價值"
-#: taiga/projects/models.py:695
+#: taiga/projects/models.py:743
msgid "default owner's role"
msgstr "預設所有者角色"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:761
msgid "default options"
msgstr "預設選項"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:762
+msgid "epic statuses"
+msgstr ""
+
+#: taiga/projects/models.py:763
msgid "us statuses"
msgstr "我們狀況"
-#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
-#: taiga/projects/userstories/models.py:74
+#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44
+#: taiga/projects/userstories/models.py:77
msgid "points"
msgstr "點數"
-#: taiga/projects/models.py:714
+#: taiga/projects/models.py:765
msgid "task statuses"
msgstr "任務狀況"
-#: taiga/projects/models.py:715
+#: taiga/projects/models.py:766
msgid "issue statuses"
msgstr "問題狀況"
-#: taiga/projects/models.py:716
+#: taiga/projects/models.py:767
msgid "issue types"
msgstr "問題類型"
-#: taiga/projects/models.py:717
+#: taiga/projects/models.py:768
msgid "priorities"
msgstr "優先性"
-#: taiga/projects/models.py:718
+#: taiga/projects/models.py:769
msgid "severities"
msgstr "嚴重性"
-#: taiga/projects/models.py:719
+#: taiga/projects/models.py:770
msgid "roles"
msgstr "角色"
-#: taiga/projects/notifications/choices.py:29
+#: taiga/projects/notifications/choices.py:30
msgid "Involved"
msgstr "相關涉入者"
-#: taiga/projects/notifications/choices.py:30
+#: taiga/projects/notifications/choices.py:31
msgid "All"
msgstr "所有"
-#: taiga/projects/notifications/choices.py:31
+#: taiga/projects/notifications/choices.py:32
msgid "None"
msgstr "無"
-#: taiga/projects/notifications/models.py:63
+#: taiga/projects/notifications/models.py:64
msgid "created date time"
msgstr "創建日期時間"
-#: taiga/projects/notifications/models.py:67
+#: taiga/projects/notifications/models.py:68
msgid "history entries"
msgstr "歷史輸入"
-#: taiga/projects/notifications/models.py:70
+#: taiga/projects/notifications/models.py:71
msgid "notify users"
msgstr "通知用戶"
-#: taiga/projects/notifications/models.py:92
#: taiga/projects/notifications/models.py:93
+#: taiga/projects/notifications/models.py:94
msgid "Watched"
msgstr "已觀注"
-#: taiga/projects/notifications/services.py:64
-#: taiga/projects/notifications/services.py:78
+#: taiga/projects/notifications/services.py:65
+#: taiga/projects/notifications/services.py:79
msgid "Notify exists for specified user and project"
msgstr "通知特定使用者與專案退出"
-#: taiga/projects/notifications/services.py:427
+#: taiga/projects/notifications/services.py:426
msgid "Invalid value for notify level"
msgstr "通知水平的無效值"
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic updated
\n"
+" Hello %(user)s,
%(changer)s has updated a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3
+#, python-format
+msgid ""
+"\n"
+"Epic updated\n"
+"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" New epic created
\n"
+" Hello %(user)s,
%(changer)s has created a new epic on "
+"%(project)s
\n"
+" Epic #%(ref)s %(subject)s
\n"
+" See epic\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"New epic created\n"
+"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n"
+"See epic #%(ref)s %(subject)s at %(url)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4
+#, python-format
+msgid ""
+"\n"
+" Epic deleted
\n"
+" Hello %(user)s,
%(changer)s has deleted a epic on %(project)s"
+"p>\n"
+"
Epic #%(ref)s %(subject)s
\n"
+" The Taiga Team
\n"
+" "
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1
+#, python-format
+msgid ""
+"\n"
+"Epic deleted\n"
+"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n"
+"Epic #%(ref)s %(subject)s\n"
+"\n"
+"---\n"
+"The Taiga Team\n"
+msgstr ""
+
+#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1
+#, python-format
+msgid ""
+"\n"
+"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n"
+msgstr ""
+
#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4
#, python-format
msgid ""
@@ -2747,159 +2835,179 @@ msgstr ""
"\n"
"[%(project)s] 刪除維基頁 \"%(page)s\"\n"
-#: taiga/projects/notifications/validators.py:47
+#: taiga/projects/notifications/validators.py:48
msgid "Watchers contains invalid users"
msgstr "監督者包含無效使用者"
-#: taiga/projects/occ/mixins.py:36
+#: taiga/projects/occ/mixins.py:37
msgid "The version must be an integer"
msgstr "版本須為整數值 "
-#: taiga/projects/occ/mixins.py:59
+#: taiga/projects/occ/mixins.py:60
msgid "The version parameter is not valid"
msgstr "本版本參數無效"
-#: taiga/projects/occ/mixins.py:75
+#: taiga/projects/occ/mixins.py:76
msgid "The version doesn't match with the current one"
msgstr "版本與目前使用不相符"
-#: taiga/projects/occ/mixins.py:94
+#: taiga/projects/occ/mixins.py:95
msgid "version"
msgstr "版本"
-#: taiga/projects/permissions.py:40
+#: taiga/projects/permissions.py:44
msgid ""
"You can't leave the project if you are the owner or there are no more admins"
msgstr ""
-#: taiga/projects/serializers.py:172
-msgid "Email address is already taken"
-msgstr "電子郵件已使用"
-
-#: taiga/projects/serializers.py:184
-msgid "Invalid role for the project"
-msgstr "專案無效的角色"
-
-#: taiga/projects/serializers.py:195
-msgid "The project owner must be admin."
+#: taiga/projects/services/members.py:118
+msgid "Project without owner"
msgstr ""
-#: taiga/projects/serializers.py:198
-msgid "At least one user must be an active admin for this project."
-msgstr ""
-
-#: taiga/projects/serializers.py:396
-msgid "Default options"
-msgstr "預設選項"
-
-#: taiga/projects/serializers.py:397
-msgid "User story's statuses"
-msgstr "使用者故事狀態"
-
-#: taiga/projects/serializers.py:398
-msgid "Points"
-msgstr "點數"
-
-#: taiga/projects/serializers.py:399
-msgid "Task's statuses"
-msgstr "任務狀態"
-
-#: taiga/projects/serializers.py:400
-msgid "Issue's statuses"
-msgstr "問題狀態"
-
-#: taiga/projects/serializers.py:401
-msgid "Issue's types"
-msgstr "問題類型"
-
-#: taiga/projects/serializers.py:402
-msgid "Priorities"
-msgstr "優先性"
-
-#: taiga/projects/serializers.py:403
-msgid "Severities"
-msgstr "嚴重性"
-
-#: taiga/projects/serializers.py:404
-msgid "Roles"
-msgstr "角色"
-
-#: taiga/projects/services/members.py:116
+#: taiga/projects/services/members.py:123
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:120
+#: taiga/projects/services/members.py:127
msgid "You have reached your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/projects.py:69
-#: taiga/projects/services/projects.py:106 taiga/users/services.py:582
+#: taiga/projects/services/projects.py:94
+#: taiga/projects/services/projects.py:134 taiga/users/services.py:589
msgid "You can't have more private projects"
msgstr ""
-#: taiga/projects/services/projects.py:73
-#: taiga/projects/services/projects.py:110 taiga/users/services.py:585
+#: taiga/projects/services/projects.py:98
+#: taiga/projects/services/projects.py:138 taiga/users/services.py:592
msgid ""
"This project reaches your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/projects.py:77
-#: taiga/projects/services/projects.py:114 taiga/users/services.py:589
+#: taiga/projects/services/projects.py:102
+#: taiga/projects/services/projects.py:142 taiga/users/services.py:596
msgid "You can't have more public projects"
msgstr ""
-#: taiga/projects/services/projects.py:81
-#: taiga/projects/services/projects.py:118 taiga/users/services.py:592
+#: taiga/projects/services/projects.py:106
+#: taiga/projects/services/projects.py:146 taiga/users/services.py:599
msgid ""
"This project reaches your current limit of memberships for public projects"
msgstr ""
-#: taiga/projects/services/stats.py:196
+#: taiga/projects/services/stats.py:197
msgid "Future sprint"
msgstr "未來之衝刺"
-#: taiga/projects/services/stats.py:216
+#: taiga/projects/services/stats.py:217
msgid "Project End"
msgstr "專案結束"
-#: taiga/projects/services/transfer.py:61
-#: taiga/projects/services/transfer.py:68
-#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169
-#: taiga/users/api.py:174
+#: taiga/projects/services/transfer.py:62
+#: taiga/projects/services/transfer.py:69
+#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186
+#: taiga/users/api.py:191
msgid "Token is invalid"
msgstr "代號無效"
-#: taiga/projects/services/transfer.py:66
+#: taiga/projects/services/transfer.py:67
msgid "Token has expired"
msgstr ""
-#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122
+#: taiga/projects/tagging/fields.py:52
+#, python-brace-format
+msgid "Invalid tag '{value}'. The color is not a valid HEX color or null."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:55
+#, python-brace-format
+msgid ""
+"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/"
+"\" | null]'."
+msgstr ""
+
+#: taiga/projects/tagging/fields.py:77
+#, python-brace-format
+msgid "Invalid tag '{value}'. It must be the tag name."
+msgstr ""
+
+#: taiga/projects/tagging/models.py:27
+msgid "tags"
+msgstr "標籤"
+
+#: taiga/projects/tagging/models.py:35
+msgid "tags colors"
+msgstr "標籤顏色"
+
+#: taiga/projects/tagging/validators.py:47
+#: taiga/projects/tagging/validators.py:74
+msgid "This tag already exists."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:54
+#: taiga/projects/tagging/validators.py:81
+msgid "The color is not a valid HEX color."
+msgstr ""
+
+#: taiga/projects/tagging/validators.py:67
+#: taiga/projects/tagging/validators.py:101
+#: taiga/projects/tagging/validators.py:114
+#: taiga/projects/tagging/validators.py:121
+msgid "The tag doesn't exist."
+msgstr ""
+
+#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106
msgid "You don't have permissions to set this sprint to this task."
msgstr "無權限更動此任務下的衝刺任務"
-#: taiga/projects/tasks/api.py:116
+#: taiga/projects/tasks/api.py:100
msgid "You don't have permissions to set this user story to this task."
msgstr "無權限更動此務下的使用者故事"
-#: taiga/projects/tasks/api.py:119
+#: taiga/projects/tasks/api.py:103
msgid "You don't have permissions to set this status to this task."
msgstr "無權限更動此任務下的狀態"
-#: taiga/projects/tasks/models.py:57
+#: taiga/projects/tasks/models.py:58
msgid "us order"
msgstr "使用者故事次序"
-#: taiga/projects/tasks/models.py:59
+#: taiga/projects/tasks/models.py:60
msgid "taskboard order"
msgstr "任務板次序"
-#: taiga/projects/tasks/models.py:67
+#: taiga/projects/tasks/models.py:68
msgid "is iocaine"
msgstr "挑戰全新任務"
-#: taiga/projects/tasks/validators.py:12
-msgid "There's no task with that id"
-msgstr "該用戶無任務 "
+#: taiga/projects/tasks/validators.py:59
+msgid "Invalid milestone id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:70
+msgid "Invalid task status id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:83
+msgid "Invalid user story id."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:107
+msgid "Invalid task status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:121
+msgid "Invalid user story id. The user story must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:133
+msgid "Invalid milestone id. The milestone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/tasks/validators.py:150
+msgid ""
+"Invalid task ids. All tasks must belong to the same project and, if it "
+"exists, to the same status, user story and/or milestone."
+msgstr ""
#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6
#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4
@@ -3277,12 +3385,12 @@ msgid ""
msgstr ""
#. Translators: Name of scrum project template.
-#: taiga/projects/translations.py:29
+#: taiga/projects/translations.py:30
msgid "Scrum"
msgstr "Scrum"
#. Translators: Description of scrum project template.
-#: taiga/projects/translations.py:31
+#: taiga/projects/translations.py:32
msgid ""
"The agile product backlog in Scrum is a prioritized features list, "
"containing short descriptions of all functionality desired in the product. "
@@ -3296,12 +3404,12 @@ msgstr ""
"客戶中學到的回應,加以改變或調整。"
#. Translators: Name of kanban project template.
-#: taiga/projects/translations.py:34
+#: taiga/projects/translations.py:35
msgid "Kanban"
msgstr "Kanban"
#. Translators: Description of kanban project template.
-#: taiga/projects/translations.py:36
+#: taiga/projects/translations.py:37
msgid ""
"Kanban is a method for managing knowledge work with an emphasis on just-in-"
"time delivery while not overloading the team members. In this approach, the "
@@ -3312,303 +3420,388 @@ msgstr ""
"定義任務到其傳送給客戶的過程,以參與者可以看到且成員從次序排列上推動來呈現"
#. Translators: User story point value (value = undefined)
-#: taiga/projects/translations.py:44
+#: taiga/projects/translations.py:45
msgid "?"
msgstr "?"
#. Translators: User story point value (value = 0)
-#: taiga/projects/translations.py:46
+#: taiga/projects/translations.py:47
msgid "0"
msgstr "0"
#. Translators: User story point value (value = 0.5)
-#: taiga/projects/translations.py:48
+#: taiga/projects/translations.py:49
msgid "1/2"
msgstr "1/2"
#. Translators: User story point value (value = 1)
-#: taiga/projects/translations.py:50
+#: taiga/projects/translations.py:51
msgid "1"
msgstr "1"
#. Translators: User story point value (value = 2)
-#: taiga/projects/translations.py:52
+#: taiga/projects/translations.py:53
msgid "2"
msgstr "2"
#. Translators: User story point value (value = 3)
-#: taiga/projects/translations.py:54
+#: taiga/projects/translations.py:55
msgid "3"
msgstr "3"
#. Translators: User story point value (value = 5)
-#: taiga/projects/translations.py:56
+#: taiga/projects/translations.py:57
msgid "5"
msgstr "5"
#. Translators: User story point value (value = 8)
-#: taiga/projects/translations.py:58
+#: taiga/projects/translations.py:59
msgid "8"
msgstr "8"
#. Translators: User story point value (value = 10)
-#: taiga/projects/translations.py:60
+#: taiga/projects/translations.py:61
msgid "10"
msgstr "10"
#. Translators: User story point value (value = 13)
-#: taiga/projects/translations.py:62
+#: taiga/projects/translations.py:63
msgid "13"
msgstr "13"
#. Translators: User story point value (value = 20)
-#: taiga/projects/translations.py:64
+#: taiga/projects/translations.py:65
msgid "20"
msgstr "20"
#. Translators: User story point value (value = 40)
-#: taiga/projects/translations.py:66
+#: taiga/projects/translations.py:67
msgid "40"
msgstr "40"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:74 taiga/projects/translations.py:97
-#: taiga/projects/translations.py:113
+#: taiga/projects/translations.py:75 taiga/projects/translations.py:98
+#: taiga/projects/translations.py:114
msgid "New"
msgstr "新 "
#. Translators: User story status
-#: taiga/projects/translations.py:77
+#: taiga/projects/translations.py:78
msgid "Ready"
msgstr "準備好了"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:80 taiga/projects/translations.py:99
-#: taiga/projects/translations.py:115
+#: taiga/projects/translations.py:81 taiga/projects/translations.py:100
+#: taiga/projects/translations.py:116
msgid "In progress"
msgstr "進行中"
#. Translators: User story status
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:83 taiga/projects/translations.py:101
-#: taiga/projects/translations.py:117
+#: taiga/projects/translations.py:84 taiga/projects/translations.py:102
+#: taiga/projects/translations.py:118
msgid "Ready for test"
msgstr "準備測試 "
#. Translators: User story status
-#: taiga/projects/translations.py:86
+#: taiga/projects/translations.py:87
msgid "Done"
msgstr "完成"
#. Translators: User story status
-#: taiga/projects/translations.py:89
+#: taiga/projects/translations.py:90
msgid "Archived"
msgstr "歸檔"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:103 taiga/projects/translations.py:119
+#: taiga/projects/translations.py:104 taiga/projects/translations.py:120
msgid "Closed"
msgstr "關閉"
#. Translators: Task status
#. Translators: Issue status
-#: taiga/projects/translations.py:105 taiga/projects/translations.py:121
+#: taiga/projects/translations.py:106 taiga/projects/translations.py:122
msgid "Needs Info"
msgstr "需求資訊"
#. Translators: Issue status
-#: taiga/projects/translations.py:123
+#: taiga/projects/translations.py:124
msgid "Postponed"
msgstr "延後"
#. Translators: Issue status
-#: taiga/projects/translations.py:125
+#: taiga/projects/translations.py:126
msgid "Rejected"
msgstr "拒絕 "
#. Translators: Issue type
-#: taiga/projects/translations.py:133
+#: taiga/projects/translations.py:134
msgid "Bug"
msgstr "系統錯誤"
#. Translators: Issue type
-#: taiga/projects/translations.py:135
+#: taiga/projects/translations.py:136
msgid "Question"
msgstr "問題"
#. Translators: Issue type
-#: taiga/projects/translations.py:137
+#: taiga/projects/translations.py:138
msgid "Enhancement"
msgstr "強化"
#. Translators: Issue priority
-#: taiga/projects/translations.py:145
+#: taiga/projects/translations.py:146
msgid "Low"
msgstr "低"
#. Translators: Issue priority
#. Translators: Issue severity
-#: taiga/projects/translations.py:147 taiga/projects/translations.py:160
+#: taiga/projects/translations.py:148 taiga/projects/translations.py:161
msgid "Normal"
msgstr "一般"
#. Translators: Issue priority
-#: taiga/projects/translations.py:149
+#: taiga/projects/translations.py:150
msgid "High"
msgstr "高"
#. Translators: Issue severity
-#: taiga/projects/translations.py:156
+#: taiga/projects/translations.py:157
msgid "Wishlist"
msgstr "願望清單"
#. Translators: Issue severity
-#: taiga/projects/translations.py:158
+#: taiga/projects/translations.py:159
msgid "Minor"
msgstr "次要"
#. Translators: Issue severity
-#: taiga/projects/translations.py:162
+#: taiga/projects/translations.py:163
msgid "Important"
msgstr "重要"
#. Translators: Issue severity
-#: taiga/projects/translations.py:164
+#: taiga/projects/translations.py:165
msgid "Critical"
msgstr "關鍵"
#. Translators: User role
-#: taiga/projects/translations.py:171
+#: taiga/projects/translations.py:172
msgid "UX"
msgstr "使用者介面"
#. Translators: User role
-#: taiga/projects/translations.py:173
+#: taiga/projects/translations.py:174
msgid "Design"
msgstr "設計"
#. Translators: User role
-#: taiga/projects/translations.py:175
+#: taiga/projects/translations.py:176
msgid "Front"
msgstr "前台"
#. Translators: User role
-#: taiga/projects/translations.py:177
+#: taiga/projects/translations.py:178
msgid "Back"
msgstr "後台"
#. Translators: User role
-#: taiga/projects/translations.py:179
+#: taiga/projects/translations.py:180
msgid "Product Owner"
msgstr "產品所有人"
#. Translators: User role
-#: taiga/projects/translations.py:181
+#: taiga/projects/translations.py:182
msgid "Stakeholder"
msgstr "利害關係人"
-#: taiga/projects/userstories/api.py:163
+#: taiga/projects/userstories/api.py:124
msgid "You don't have permissions to set this sprint to this user story."
msgstr "無權限更動使用者故事的衝刺任務"
-#: taiga/projects/userstories/api.py:167
+#: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this status to this user story."
msgstr "無權限更動此使用者故事的狀態"
-#: taiga/projects/userstories/api.py:267
+#: taiga/projects/userstories/api.py:218
+#, python-brace-format
+msgid "Invalid role id '{role_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:225
+#, python-brace-format
+msgid "Invalid points id '{points_id}'"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:240
#, python-brace-format
msgid "Generating the user story #{ref} - {subject}"
msgstr "産生使用者故事 #{ref} - {subject}"
-#: taiga/projects/userstories/models.py:39
+#: taiga/projects/userstories/api.py:301
+msgid "ref param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/api.py:304
+msgid "project or project_slug param is needed"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:41
msgid "role"
msgstr "角色"
-#: taiga/projects/userstories/models.py:77
+#: taiga/projects/userstories/models.py:80
msgid "backlog order"
msgstr "待辦任務先後次序"
-#: taiga/projects/userstories/models.py:79
-#: taiga/projects/userstories/models.py:81
+#: taiga/projects/userstories/models.py:82
msgid "sprint order"
msgstr "衝刺次序"
-#: taiga/projects/userstories/models.py:89
+#: taiga/projects/userstories/models.py:84
+msgid "kanban order"
+msgstr ""
+
+#: taiga/projects/userstories/models.py:92
msgid "finish date"
msgstr "完成日期"
-#: taiga/projects/userstories/models.py:97
-msgid "is client requirement"
-msgstr "客戶要求"
-
-#: taiga/projects/userstories/models.py:99
-msgid "is team requirement"
-msgstr "團隊要求"
-
-#: taiga/projects/userstories/models.py:104
+#: taiga/projects/userstories/models.py:107
msgid "generated from issue"
msgstr "産生自問題 "
-#: taiga/projects/userstories/validators.py:29
+#: taiga/projects/userstories/validators.py:43
msgid "There's no user story with that id"
msgstr "該ID無相關使用者故事"
-#: taiga/projects/validators.py:29
+#: taiga/projects/userstories/validators.py:82
+#: taiga/projects/userstories/validators.py:108
+msgid ""
+"Invalid user story status id. The status must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:120
+msgid "Invalid milestone id. The milistone must belong to the same project."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:135
+msgid ""
+"Invalid user story ids. All stories must belong to the same project and, if "
+"it exists, to the same status and milestone."
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:159
+msgid "The milestone isn't valid for the project"
+msgstr ""
+
+#: taiga/projects/userstories/validators.py:169
+msgid "All the user stories must be from the same project"
+msgstr ""
+
+#: taiga/projects/validators.py:61
msgid "There's no project with that id"
msgstr "該ID無相關專案"
-#: taiga/projects/validators.py:38
-msgid "There's no user story status with that id"
-msgstr "該ID無相關使用者故事狀態"
+#: taiga/projects/validators.py:142
+msgid "Email address is already taken"
+msgstr "電子郵件已使用"
-#: taiga/projects/validators.py:47
-msgid "There's no task status with that id"
-msgstr "該ID無相關任務狀況"
+#: taiga/projects/validators.py:154
+msgid "Invalid role for the project"
+msgstr "專案無效的角色"
-#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33
-#: taiga/projects/votes/models.py:57
+#: taiga/projects/validators.py:165
+msgid "The project owner must be admin."
+msgstr ""
+
+#: taiga/projects/validators.py:169
+msgid "At least one user must be an active admin for this project."
+msgstr ""
+
+#: taiga/projects/validators.py:201
+msgid "Invalid role ids. All roles must belong to the same project."
+msgstr ""
+
+#: taiga/projects/validators.py:225
+msgid "Default options"
+msgstr "預設選項"
+
+#: taiga/projects/validators.py:226
+msgid "User story's statuses"
+msgstr "使用者故事狀態"
+
+#: taiga/projects/validators.py:227
+msgid "Points"
+msgstr "點數"
+
+#: taiga/projects/validators.py:228
+msgid "Task's statuses"
+msgstr "任務狀態"
+
+#: taiga/projects/validators.py:229
+msgid "Issue's statuses"
+msgstr "問題狀態"
+
+#: taiga/projects/validators.py:230
+msgid "Issue's types"
+msgstr "問題類型"
+
+#: taiga/projects/validators.py:231
+msgid "Priorities"
+msgstr "優先性"
+
+#: taiga/projects/validators.py:232
+msgid "Severities"
+msgstr "嚴重性"
+
+#: taiga/projects/validators.py:233
+msgid "Roles"
+msgstr "角色"
+
+#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34
+#: taiga/projects/votes/models.py:58
msgid "Votes"
msgstr "投票數"
-#: taiga/projects/votes/models.py:56
+#: taiga/projects/votes/models.py:57
msgid "Vote"
msgstr "投票 "
-#: taiga/projects/wiki/api.py:70
+#: taiga/projects/wiki/api.py:77
msgid "'content' parameter is mandatory"
msgstr "'content'參數為必要"
-#: taiga/projects/wiki/api.py:73
+#: taiga/projects/wiki/api.py:80
msgid "'project_id' parameter is mandatory"
msgstr "'project_id'參數為必要"
-#: taiga/projects/wiki/models.py:38
+#: taiga/projects/wiki/models.py:42
msgid "last modifier"
msgstr "上次更改"
-#: taiga/projects/wiki/models.py:71
+#: taiga/projects/wiki/models.py:75
msgid "href"
msgstr "href"
-#: taiga/timeline/signals.py:68
+#: taiga/timeline/signals.py:63
msgid "Check the history API for the exact diff"
msgstr "檢查API過去資料以找出差異"
-#: taiga/users/admin.py:38
+#: taiga/users/admin.py:39
msgid "Project Member"
msgstr ""
-#: taiga/users/admin.py:39
+#: taiga/users/admin.py:40
msgid "Project Members"
msgstr ""
-#: taiga/users/admin.py:49
+#: taiga/users/admin.py:50
msgid "id"
msgstr ""
@@ -3636,145 +3829,137 @@ msgstr ""
msgid "Important dates"
msgstr "重要日期"
-#: taiga/users/api.py:113
+#: taiga/users/api.py:123
msgid "Duplicated email"
msgstr "複製電子郵件"
-#: taiga/users/api.py:115
+#: taiga/users/api.py:125
msgid "Not valid email"
msgstr "非有效電子郵性"
-#: taiga/users/api.py:148
+#: taiga/users/api.py:165
msgid "Invalid username or email"
msgstr "無效使用者或郵件"
-#: taiga/users/api.py:157
+#: taiga/users/api.py:174
msgid "Mail sended successful!"
msgstr "成功送出郵件"
-#: taiga/users/api.py:195
+#: taiga/users/api.py:212
msgid "Current password parameter needed"
msgstr "需要目前密碼之參數"
-#: taiga/users/api.py:198
+#: taiga/users/api.py:215
msgid "New password parameter needed"
msgstr "需要新密碼參數"
-#: taiga/users/api.py:201
+#: taiga/users/api.py:218
msgid "Invalid password length at least 6 charaters needed"
msgstr "無效密碼長度,至少需6個字元"
-#: taiga/users/api.py:204
+#: taiga/users/api.py:221
msgid "Invalid current password"
msgstr "無效密碼"
-#: taiga/users/api.py:251 taiga/users/api.py:257
+#: taiga/users/api.py:268 taiga/users/api.py:274
msgid ""
"Invalid, are you sure the token is correct and you didn't use it before?"
msgstr "無效,請確認代號正確,之前是否曾使用過?"
-#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295
+#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312
msgid "Invalid, are you sure the token is correct?"
msgstr "無效,請確認代號是否正確?"
-#: taiga/users/models.py:96
+#: taiga/users/models.py:95
msgid "superuser status"
msgstr "超級使用者狀態 "
-#: taiga/users/models.py:97
+#: taiga/users/models.py:96
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr "無經明確分派,即賦予該使用者所有權限,"
-#: taiga/users/models.py:127
+#: taiga/users/models.py:126
msgid "username"
msgstr "使用者名稱"
-#: taiga/users/models.py:128
+#: taiga/users/models.py:127
msgid ""
"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters"
msgstr "必填。最多30字元(可為數字,字母,符號....)"
-#: taiga/users/models.py:131
+#: taiga/users/models.py:130
msgid "Enter a valid username."
msgstr "輸入有效的使用者名稱 "
-#: taiga/users/models.py:134
+#: taiga/users/models.py:133
msgid "active"
msgstr "活躍"
-#: taiga/users/models.py:135
+#: taiga/users/models.py:134
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr "賦予該使用者活躍角色,以不選擇取代刪除帳戶功能。"
-#: taiga/users/models.py:141
+#: taiga/users/models.py:140
msgid "biography"
msgstr "自傳"
-#: taiga/users/models.py:144
+#: taiga/users/models.py:143
msgid "photo"
msgstr "照片"
-#: taiga/users/models.py:145
+#: taiga/users/models.py:144
msgid "date joined"
msgstr "加入日期"
-#: taiga/users/models.py:147
+#: taiga/users/models.py:146
msgid "default language"
msgstr "預設語言 "
-#: taiga/users/models.py:149
+#: taiga/users/models.py:148
msgid "default theme"
msgstr "預設主題"
-#: taiga/users/models.py:151
+#: taiga/users/models.py:150
msgid "default timezone"
msgstr "預設時區"
-#: taiga/users/models.py:153
+#: taiga/users/models.py:152
msgid "colorize tags"
msgstr "顏色標籤"
-#: taiga/users/models.py:158
+#: taiga/users/models.py:157
msgid "email token"
msgstr "電子郵件符號 "
-#: taiga/users/models.py:160
+#: taiga/users/models.py:159
msgid "new email address"
msgstr "新電子郵件地址"
-#: taiga/users/models.py:167
+#: taiga/users/models.py:166
msgid "max number of owned private projects"
msgstr ""
-#: taiga/users/models.py:170
+#: taiga/users/models.py:169
msgid "max number of owned public projects"
msgstr ""
-#: taiga/users/models.py:173
+#: taiga/users/models.py:172
msgid "max number of memberships for each owned private project"
msgstr ""
-#: taiga/users/models.py:177
+#: taiga/users/models.py:176
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:297
+#: taiga/users/models.py:296
msgid "permissions"
msgstr "許可"
-#: taiga/users/serializers.py:65
-msgid "invalid"
-msgstr "無效"
-
-#: taiga/users/serializers.py:76
-msgid "Invalid username. Try with a different one."
-msgstr "無效使用者名稱,請重試其它名稱 "
-
-#: taiga/users/services.py:53 taiga/users/services.py:70
+#: taiga/users/services.py:51 taiga/users/services.py:68
msgid "Username or password does not matches user."
msgstr "用戶名稱與密碼不符"
@@ -3951,47 +4136,51 @@ msgstr ""
msgid "You've been Taigatized!"
msgstr "您已加入Taiga"
-#: taiga/users/validators.py:30
-msgid "There's no role with that id"
-msgstr "該用戶無角色"
+#: taiga/users/validators.py:45
+msgid "invalid"
+msgstr "無效"
-#: taiga/userstorage/api.py:51
+#: taiga/users/validators.py:56
+msgid "Invalid username. Try with a different one."
+msgstr "無效使用者名稱,請重試其它名稱 "
+
+#: taiga/userstorage/api.py:53
msgid ""
"Duplicate key value violates unique constraint. Key '{}' already exists."
msgstr "複製的關鍵值侵害獨特約束 關鍵值'{}' 已存在"
-#: taiga/userstorage/models.py:31
+#: taiga/userstorage/models.py:32
msgid "key"
msgstr "關鍵值"
-#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39
+#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40
msgid "URL"
msgstr "URL"
-#: taiga/webhooks/models.py:30
+#: taiga/webhooks/models.py:31
msgid "secret key"
msgstr "袐密代碼"
-#: taiga/webhooks/models.py:40
+#: taiga/webhooks/models.py:41
msgid "status code"
msgstr "狀態碼"
-#: taiga/webhooks/models.py:41
+#: taiga/webhooks/models.py:42
msgid "request data"
msgstr "要求資料"
-#: taiga/webhooks/models.py:42
+#: taiga/webhooks/models.py:43
msgid "request headers"
msgstr "要求標頭"
-#: taiga/webhooks/models.py:43
+#: taiga/webhooks/models.py:44
msgid "response data"
msgstr "回應資料"
-#: taiga/webhooks/models.py:44
+#: taiga/webhooks/models.py:45
msgid "response headers"
msgstr "回應標頭 "
-#: taiga/webhooks/models.py:45
+#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "期間"
diff --git a/taiga/mdrender/extensions/references.py b/taiga/mdrender/extensions/references.py
index d472d663..6828c739 100644
--- a/taiga/mdrender/extensions/references.py
+++ b/taiga/mdrender/extensions/references.py
@@ -57,7 +57,9 @@ class TaigaReferencesPattern(Pattern):
subject = instance.content_object.subject
- if instance.content_type.model == "userstory":
+ if instance.content_type.model == "epic":
+ html_classes = "reference epic"
+ elif instance.content_type.model == "userstory":
html_classes = "reference user-story"
elif instance.content_type.model == "task":
html_classes = "reference task"
diff --git a/taiga/mdrender/service.py b/taiga/mdrender/service.py
index cc87e25b..701ed0d4 100644
--- a/taiga/mdrender/service.py
+++ b/taiga/mdrender/service.py
@@ -126,16 +126,42 @@ def render_and_extract(project, text):
class DiffMatchPatch(diff_match_patch.diff_match_patch):
def diff_pretty_html(self, diffs):
+ def _sanitize_text(text):
+ return (text.replace("&", "&").replace("<", "<")
+ .replace(">", ">").replace("\n", "
"))
+
+ def _split_long_text(text, idx, size):
+ splited_text = text.split()
+
+ if len(splited_text) > 25:
+ if idx == 0:
+ # The first is (...)text
+ first = ""
+ else:
+ first = " ".join(splited_text[:10])
+
+ if idx != 0 and idx == size - 1:
+ # The last is text(...)
+ last = ""
+ else:
+ last = " ".join(splited_text[-10:])
+
+ return "{}(...){}".format(first, last)
+ return text
+
+ size = len(diffs)
html = []
- for (op, data) in diffs:
- text = (data.replace("&", "&").replace("<", "<")
- .replace(">", ">").replace("\n", "
"))
+ for idx, (op, data) in enumerate(diffs):
if op == self.DIFF_INSERT:
- html.append("%s" % text)
+ text = _sanitize_text(data)
+ html.append("{}".format(text))
elif op == self.DIFF_DELETE:
- html.append("%s" % text)
+ text = _sanitize_text(data)
+ html.append("{}".format(text))
elif op == self.DIFF_EQUAL:
- html.append("%s" % text)
+ text = _split_long_text(_sanitize_text(data), idx, size)
+ html.append("{}".format(text))
+
return "".join(html)
diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py
new file mode 100644
index 00000000..594d48ee
--- /dev/null
+++ b/taiga/permissions/choices.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.utils.translation import ugettext_lazy as _
+
+ANON_PERMISSIONS = [
+ ('view_project', _('View project')),
+ ('view_milestones', _('View milestones')),
+ ('view_epics', _('View epic')),
+ ('view_us', _('View user stories')),
+ ('view_tasks', _('View tasks')),
+ ('view_issues', _('View issues')),
+ ('view_wiki_pages', _('View wiki pages')),
+ ('view_wiki_links', _('View wiki links')),
+]
+
+MEMBERS_PERMISSIONS = [
+ ('view_project', _('View project')),
+ # Milestone permissions
+ ('view_milestones', _('View milestones')),
+ ('add_milestone', _('Add milestone')),
+ ('modify_milestone', _('Modify milestone')),
+ ('delete_milestone', _('Delete milestone')),
+ # Epic permissions
+ ('view_epics', _('View epic')),
+ ('add_epic', _('Add epic')),
+ ('modify_epic', _('Modify epic')),
+ ('comment_epic', _('Comment epic')),
+ ('delete_epic', _('Delete epic')),
+ # US permissions
+ ('view_us', _('View user story')),
+ ('add_us', _('Add user story')),
+ ('modify_us', _('Modify user story')),
+ ('comment_us', _('Comment user story')),
+ ('delete_us', _('Delete user story')),
+ # Task permissions
+ ('view_tasks', _('View tasks')),
+ ('add_task', _('Add task')),
+ ('modify_task', _('Modify task')),
+ ('comment_task', _('Comment task')),
+ ('delete_task', _('Delete task')),
+ # Issue permissions
+ ('view_issues', _('View issues')),
+ ('add_issue', _('Add issue')),
+ ('modify_issue', _('Modify issue')),
+ ('comment_issue', _('Comment issue')),
+ ('delete_issue', _('Delete issue')),
+ # Wiki page permissions
+ ('view_wiki_pages', _('View wiki pages')),
+ ('add_wiki_page', _('Add wiki page')),
+ ('modify_wiki_page', _('Modify wiki page')),
+ ('comment_wiki_page', _('Comment wiki page')),
+ ('delete_wiki_page', _('Delete wiki page')),
+ # Wiki link permissions
+ ('view_wiki_links', _('View wiki links')),
+ ('add_wiki_link', _('Add wiki link')),
+ ('modify_wiki_link', _('Modify wiki link')),
+ ('delete_wiki_link', _('Delete wiki link')),
+]
+
+ADMINS_PERMISSIONS = [
+ ('modify_project', _('Modify project')),
+ ('delete_project', _('Delete project')),
+ ('add_member', _('Add member')),
+ ('remove_member', _('Remove member')),
+ ('admin_project_values', _('Admin project values')),
+ ('admin_roles', _('Admin roles')),
+]
diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py
index b45ed4ef..0ffefe40 100644
--- a/taiga/permissions/permissions.py
+++ b/taiga/permissions/permissions.py
@@ -17,77 +17,75 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.utils.translation import ugettext_lazy as _
+from django.apps import apps
-ANON_PERMISSIONS = [
- ('view_project', _('View project')),
- ('view_milestones', _('View milestones')),
- ('view_us', _('View user stories')),
- ('view_tasks', _('View tasks')),
- ('view_issues', _('View issues')),
- ('view_wiki_pages', _('View wiki pages')),
- ('view_wiki_links', _('View wiki links')),
-]
+from taiga.base.api.permissions import PermissionComponent
-USER_PERMISSIONS = [
- ('view_project', _('View project')),
- ('view_milestones', _('View milestones')),
- ('view_us', _('View user stories')),
- ('view_issues', _('View issues')),
- ('view_tasks', _('View tasks')),
- ('view_wiki_pages', _('View wiki pages')),
- ('view_wiki_links', _('View wiki links')),
- ('request_membership', _('Request membership')),
- ('add_us_to_project', _('Add user story to project')),
- ('add_comments_to_us', _('Add comments to user stories')),
- ('add_comments_to_task', _('Add comments to tasks')),
- ('add_issue', _('Add issues')),
- ('add_comments_to_issue', _('Add comments to issues')),
- ('add_wiki_page', _('Add wiki page')),
- ('modify_wiki_page', _('Modify wiki page')),
- ('add_wiki_link', _('Add wiki link')),
- ('modify_wiki_link', _('Modify wiki link')),
-]
+from . import services
-MEMBERS_PERMISSIONS = [
- ('view_project', _('View project')),
- # Milestone permissions
- ('view_milestones', _('View milestones')),
- ('add_milestone', _('Add milestone')),
- ('modify_milestone', _('Modify milestone')),
- ('delete_milestone', _('Delete milestone')),
- # US permissions
- ('view_us', _('View user story')),
- ('add_us', _('Add user story')),
- ('modify_us', _('Modify user story')),
- ('delete_us', _('Delete user story')),
- # Task permissions
- ('view_tasks', _('View tasks')),
- ('add_task', _('Add task')),
- ('modify_task', _('Modify task')),
- ('delete_task', _('Delete task')),
- # Issue permissions
- ('view_issues', _('View issues')),
- ('add_issue', _('Add issue')),
- ('modify_issue', _('Modify issue')),
- ('delete_issue', _('Delete issue')),
- # Wiki page permissions
- ('view_wiki_pages', _('View wiki pages')),
- ('add_wiki_page', _('Add wiki page')),
- ('modify_wiki_page', _('Modify wiki page')),
- ('delete_wiki_page', _('Delete wiki page')),
- # Wiki link permissions
- ('view_wiki_links', _('View wiki links')),
- ('add_wiki_link', _('Add wiki link')),
- ('modify_wiki_link', _('Modify wiki link')),
- ('delete_wiki_link', _('Delete wiki link')),
-]
-ADMINS_PERMISSIONS = [
- ('modify_project', _('Modify project')),
- ('add_member', _('Add member')),
- ('remove_member', _('Remove member')),
- ('delete_project', _('Delete project')),
- ('admin_project_values', _('Admin project values')),
- ('admin_roles', _('Admin roles')),
-]
+######################################################################
+# Generic perms
+######################################################################
+
+class HasProjectPerm(PermissionComponent):
+ def __init__(self, perm, *components):
+ self.project_perm = perm
+ super().__init__(*components)
+
+ def check_permissions(self, request, view, obj=None):
+ return services.user_has_perm(request.user, self.project_perm, obj)
+
+
+class IsObjectOwner(PermissionComponent):
+ def check_permissions(self, request, view, obj=None):
+ if obj.owner is None:
+ return False
+
+ return obj.owner == request.user
+
+
+######################################################################
+# Project Perms
+######################################################################
+
+class IsProjectAdmin(PermissionComponent):
+ def check_permissions(self, request, view, obj=None):
+ return services.is_project_admin(request.user, obj)
+
+
+######################################################################
+# Common perms for stories, tasks and issues
+######################################################################
+
+class CommentAndOrUpdatePerm(PermissionComponent):
+ def __init__(self, update_perm, comment_perm, *components):
+ self.update_perm = update_perm
+ self.comment_perm = comment_perm
+ super().__init__(*components)
+
+ def check_permissions(self, request, view, obj=None):
+ if not obj:
+ return False
+
+ project_id = request.DATA.get('project', None)
+ if project_id and obj.project_id != project_id:
+ project = apps.get_model("projects", "Project").objects.get(pk=project_id)
+ else:
+ project = obj.project
+
+ data_keys = request.DATA.keys()
+
+ if (not services.user_has_perm(request.user, self.comment_perm, project) and
+ "comment" in data_keys):
+ # User can't comment but there is a comment in the request
+ #raise exc.PermissionDenied(_("You don't have permissions to comment this."))
+ return False
+
+ if (not services.user_has_perm(request.user, self.update_perm, project) and
+ len(data_keys - "comment")):
+ # User can't update but there is a change in the request
+ #raise exc.PermissionDenied(_("You don't have permissions to update this."))
+ return False
+
+ return True
diff --git a/taiga/permissions/service.py b/taiga/permissions/services.py
similarity index 75%
rename from taiga/permissions/service.py
rename to taiga/permissions/services.py
index 90aa1bc5..50d8d72d 100644
--- a/taiga/permissions/service.py
+++ b/taiga/permissions/services.py
@@ -17,10 +17,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
+from .choices import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from django.apps import apps
+
def _get_user_project_membership(user, project, cache="user"):
"""
cache param determines how memberships are calculated trying to reuse the existing data
@@ -77,58 +78,69 @@ def user_has_perm(user, perm, obj=None, cache="user"):
in cache
"""
project = _get_object_project(obj)
-
if not project:
return False
return perm in get_user_project_permissions(user, project, cache=cache)
-def role_has_perm(role, perm):
- return perm in role.permissions
-
-
def _get_membership_permissions(membership):
if membership and membership.role and membership.role.permissions:
return membership.role.permissions
return []
-def get_user_project_permissions(user, project, cache="user"):
- """
- cache param determines how memberships are calculated trying to reuse the existing data
- in cache
- """
- membership = _get_user_project_membership(user, project, cache=cache)
- if user.is_superuser:
+def calculate_permissions(is_authenticated=False, is_superuser=False, is_member=False,
+ is_admin=False, role_permissions=[], anon_permissions=[],
+ public_permissions=[]):
+ if is_superuser:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
- public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS))
+ public_permissions = []
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
- elif membership:
- if membership.is_admin:
+ elif is_member:
+ if is_admin:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
else:
admins_permissions = []
members_permissions = []
- members_permissions = members_permissions + _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():
+ members_permissions = members_permissions + role_permissions
+ public_permissions = public_permissions if public_permissions is not None else []
+ anon_permissions = anon_permissions if anon_permissions is not None else []
+ elif is_authenticated:
admins_permissions = []
members_permissions = []
- public_permissions = project.public_permissions if project.public_permissions is not None else []
- anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
+ public_permissions = public_permissions if public_permissions is not None else []
+ anon_permissions = anon_permissions if anon_permissions is not None else []
else:
admins_permissions = []
members_permissions = []
public_permissions = []
- anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
+ anon_permissions = anon_permissions if anon_permissions is not None else []
return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
+def get_user_project_permissions(user, project, cache="user"):
+ """
+ cache param determines how memberships are calculated trying to reuse the existing data
+ in cache
+ """
+ membership = _get_user_project_membership(user, project, cache=cache)
+ is_member = membership is not None
+ is_admin = is_member and membership.is_admin
+ return calculate_permissions(
+ is_authenticated = user.is_authenticated(),
+ is_superuser = user.is_superuser,
+ is_member = is_member,
+ is_admin = is_admin,
+ role_permissions = _get_membership_permissions(membership),
+ anon_permissions = project.anon_permissions,
+ public_permissions = project.public_permissions
+ )
+
+
def set_base_permissions_for_project(project):
if project.is_private:
project.anon_permissions = []
diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py
index 344f2344..9f7f5431 100644
--- a/taiga/projects/admin.py
+++ b/taiga/projects/admin.py
@@ -35,6 +35,9 @@ class MembershipAdmin(admin.ModelAdmin):
list_display_links = list_display
raw_id_fields = ["project"]
+ def has_add_permission(self, request):
+ return False
+
def get_object(self, *args, **kwargs):
self.obj = super().get_object(*args, **kwargs)
return self.obj
@@ -103,8 +106,7 @@ class ProjectAdmin(admin.ModelAdmin):
(_("Extra info"), {
"classes": ("collapse",),
"fields": ("creation_template",
- ("is_looking_for_people", "looking_for_people_note"),
- "tags_colors"),
+ ("is_looking_for_people", "looking_for_people_note")),
}),
(_("Modules"), {
"classes": ("collapse",),
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 35223194..5284f44e 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -22,59 +22,58 @@ from dateutil.relativedelta import relativedelta
from django.apps import apps
from django.conf import settings
-from django.db.models import signals, Prefetch
-from django.db.models import Value as V
-from django.db.models.functions import Coalesce
-from django.core.exceptions import ValidationError
+from django.http import Http404
from django.utils.translation import ugettext as _
from django.utils import timezone
-from django.http import Http404
+
+from django_pglocks import advisory_lock
from taiga.base import filters
-from taiga.base import response
from taiga.base import exceptions as exc
-from taiga.base.decorators import list_route
-from taiga.base.decorators import detail_route
+from taiga.base import response
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin
from taiga.base.api.permissions import AllowAnyPermission
from taiga.base.api.utils import get_object_or_404
+from taiga.base.decorators import list_route
+from taiga.base.decorators import detail_route
from taiga.base.utils.slug import slugify_uniquely
+from taiga.permissions import services as permissions_services
+
+from taiga.projects.epics.models import Epic
from taiga.projects.history.mixins import HistoryResourceMixin
-from taiga.projects.notifications.models import NotifyPolicy
-from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
-from taiga.projects.notifications.choices import NotifyLevel
-
-from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
-from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
-
-from taiga.projects.userstories.models import UserStory, RolePoints
-from taiga.projects.tasks.models import Task
from taiga.projects.issues.models import Issue
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
-from taiga.permissions import service as permissions_service
-from taiga.users import services as users_service
+from taiga.projects.notifications.mixins import WatchersViewSetMixin
+from taiga.projects.notifications.choices import NotifyLevel
+from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
+from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
+from taiga.projects.tasks.models import Task
+from taiga.projects.tagging.api import TagsColorsResourceMixin
+from taiga.projects.userstories.models import UserStory, RolePoints
from . import filters as project_filters
from . import models
from . import permissions
from . import serializers
+from . import validators
from . import services
-
+from . import utils as project_utils
######################################################
-## Project
+# Project
######################################################
+
+
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
- BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet):
-
+ BlockeableSaveMixin, BlockeableDeleteMixin,
+ TagsColorsResourceMixin, ModelCrudViewSet):
+ validator_class = validators.ProjectValidator
queryset = models.Project.objects.all()
- serializer_class = serializers.ProjectDetailSerializer
- admin_serializer_class = serializers.ProjectDetailAdminSerializer
- list_serializer_class = serializers.ProjectSerializer
permission_classes = (permissions.ProjectPermission, )
- filter_backends = (project_filters.QFilterBackend,
+ filter_backends = (project_filters.UserOrderFilterBackend,
+ project_filters.QFilterBackend,
project_filters.CanViewProjectObjFilterBackend,
project_filters.DiscoverModeFilterBackend)
@@ -85,8 +84,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
"is_kanban_activated")
ordering = ("name", "id")
- order_by_fields = ("memberships__user_order",
- "total_fans",
+ order_by_fields = ("total_fans",
"total_fans_last_week",
"total_fans_last_month",
"total_fans_last_year",
@@ -106,53 +104,38 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
def get_queryset(self):
qs = super().get_queryset()
-
qs = qs.select_related("owner")
- # Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
- # so we add some custom prefetching
- qs = qs.prefetch_related("members")
- qs = qs.prefetch_related("memberships")
- qs = qs.prefetch_related(Prefetch("notify_policies",
- NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
-
- Milestone = apps.get_model("milestones", "Milestone")
- qs = qs.prefetch_related(Prefetch("milestones",
- Milestone.objects.filter(closed=True), to_attr="closed_milestones"))
+ qs = project_utils.attach_extra_info(qs, user=self.request.user)
# If filtering an activity period we must exclude the activities not updated recently enough
now = timezone.now()
order_by_field_name = self._get_order_by_field_name()
if order_by_field_name == "total_fans_last_week":
- qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1))
+ qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(weeks=1))
elif order_by_field_name == "total_fans_last_month":
- qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1))
+ qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(months=1))
elif order_by_field_name == "total_fans_last_year":
- qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1))
+ qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(years=1))
elif order_by_field_name == "total_activity_last_week":
- qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1))
+ qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(weeks=1))
elif order_by_field_name == "total_activity_last_month":
- qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1))
+ qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(months=1))
elif order_by_field_name == "total_activity_last_year":
- qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1))
+ qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(years=1))
return qs
+ def retrieve(self, request, *args, **kwargs):
+ if self.action == "by_slug":
+ self.lookup_field = "slug"
+
+ return super().retrieve(request, *args, **kwargs)
+
def get_serializer_class(self):
- serializer_class = self.serializer_class
-
if self.action == "list":
- serializer_class = self.list_serializer_class
- elif self.action != "create":
- if self.action == "by_slug":
- slug = self.request.QUERY_PARAMS.get("slug", None)
- project = get_object_or_404(models.Project, slug=slug)
- else:
- project = self.get_object()
+ return serializers.ProjectSerializer
- if permissions_service.is_project_admin(self.request.user, project):
- serializer_class = self.admin_serializer_class
-
- return serializer_class
+ return serializers.ProjectDetailSerializer
@detail_route(methods=["POST"])
def change_logo(self, request, *args, **kwargs):
@@ -215,11 +198,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
if self.request.user.is_anonymous():
return response.Unauthorized()
- serializer = serializers.UpdateProjectOrderBulkSerializer(data=request.DATA, many=True)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateProjectOrderBulkValidator(data=request.DATA, many=True)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None)
@@ -234,20 +217,22 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
if not template_description:
raise response.BadRequest(_("Not valid template description"))
- template_slug = slugify_uniquely(template_name, models.ProjectTemplate)
+ with advisory_lock("create-project-template") as acquired_key_lock:
+ template_slug = slugify_uniquely(template_name, models.ProjectTemplate)
- project = self.get_object()
+ project = self.get_object()
- self.check_permissions(request, 'create_template', project)
+ self.check_permissions(request, 'create_template', project)
- template = models.ProjectTemplate(
- name=template_name,
- slug=template_slug,
- description=template_description,
- )
+ template = models.ProjectTemplate(
+ name=template_name,
+ slug=template_slug,
+ description=template_description,
+ )
- template.load_data_from_project(project)
- template.save()
+ template.load_data_from_project(project)
+
+ template.save()
return response.Created(serializers.ProjectTemplateSerializer(template).data)
@detail_route(methods=['POST'])
@@ -258,6 +243,20 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
services.remove_user_from_project(request.user, project)
return response.Ok()
+ def _regenerate_csv_uuid(self, project, field):
+ uuid_value = uuid.uuid4().hex
+ setattr(project, field, uuid_value)
+ project.save()
+ return uuid_value
+
+ @detail_route(methods=["POST"])
+ def regenerate_epics_csv_uuid(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "regenerate_epics_csv_uuid", project)
+ self.pre_conditions_on_save(project)
+ data = {"uuid": self._regenerate_csv_uuid(project, "epics_csv_uuid")}
+ return response.Ok(data)
+
@detail_route(methods=["POST"])
def regenerate_userstories_csv_uuid(self, request, pk=None):
project = self.get_object()
@@ -266,14 +265,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")}
return response.Ok(data)
- @detail_route(methods=["POST"])
- def regenerate_issues_csv_uuid(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "regenerate_issues_csv_uuid", project)
- self.pre_conditions_on_save(project)
- data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
- return response.Ok(data)
-
@detail_route(methods=["POST"])
def regenerate_tasks_csv_uuid(self, request, pk=None):
project = self.get_object()
@@ -282,11 +273,18 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")}
return response.Ok(data)
+ @detail_route(methods=["POST"])
+ def regenerate_issues_csv_uuid(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "regenerate_issues_csv_uuid", project)
+ self.pre_conditions_on_save(project)
+ data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")}
+ return response.Ok(data)
+
@list_route(methods=["GET"])
- def by_slug(self, request):
+ def by_slug(self, request, *args, **kwargs):
slug = request.QUERY_PARAMS.get("slug", None)
- project = get_object_or_404(models.Project, slug=slug)
- return self.retrieve(request, pk=project.pk)
+ return self.retrieve(request, slug=slug)
@detail_route(methods=["GET", "PATCH"])
def modules(self, request, pk=None):
@@ -309,12 +307,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "stats", project)
return response.Ok(services.get_stats_for_project(project))
- def _regenerate_csv_uuid(self, project, field):
- uuid_value = uuid.uuid4().hex
- setattr(project, field, uuid_value)
- project.save()
- return uuid_value
-
@detail_route(methods=["GET"])
def member_stats(self, request, pk=None):
project = self.get_object()
@@ -327,12 +319,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "issues_stats", project)
return response.Ok(services.get_stats_for_project_issues(project))
- @detail_route(methods=["GET"])
- def tags_colors(self, request, pk=None):
- project = self.get_object()
- self.check_permissions(request, "tags_colors", project)
- return response.Ok(dict(project.tags_colors))
-
@detail_route(methods=["POST"])
def transfer_validate_token(self, request, pk=None):
project = self.get_object()
@@ -368,7 +354,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
return response.BadRequest(_("The user must be already a project member"))
reason = request.DATA.get('reason', None)
- transfer_token = services.start_project_transfer(project, user, reason)
+ services.start_project_transfer(project, user, reason)
return response.Ok()
@detail_route(methods=["POST"])
@@ -405,6 +391,10 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
services.reject_project_transfer(project, request.user, token, reason)
return response.Ok()
+ def _raise_if_blocked(self, project):
+ if self.is_blocked(project):
+ raise exc.Blocked(_("Blocked element"))
+
def _set_base_permissions(self, obj):
update_permissions = False
if not obj.id:
@@ -417,7 +407,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
update_permissions = True
if update_permissions:
- permissions_service.set_base_permissions_for_project(obj)
+ permissions_services.set_base_permissions_for_project(obj)
def pre_save(self, obj):
if not obj.id:
@@ -468,20 +458,21 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
## Custom values for selectors
######################################################
-class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
- ModelCrudViewSet, BulkUpdateOrderMixin):
+class EpicStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
- model = models.Points
- serializer_class = serializers.PointsSerializer
- permission_classes = (permissions.PointsPermission,)
+ model = models.EpicStatus
+ serializer_class = serializers.EpicStatusSerializer
+ validator_class = validators.EpicStatusValidator
+ permission_classes = (permissions.EpicStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
- bulk_update_param = "bulk_points"
- bulk_update_perm = "change_points"
- bulk_update_order_action = services.bulk_update_points_order
- move_on_destroy_related_class = RolePoints
- move_on_destroy_related_field = "points"
- move_on_destroy_project_default_field = "default_points"
+ bulk_update_param = "bulk_epic_statuses"
+ bulk_update_perm = "change_epicstatus"
+ bulk_update_order_action = services.bulk_update_epic_status_order
+ move_on_destroy_related_class = Epic
+ move_on_destroy_related_field = "status"
+ move_on_destroy_project_default_field = "default_epic_status"
class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
@@ -489,6 +480,7 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.UserStoryStatus
serializer_class = serializers.UserStoryStatusSerializer
+ validator_class = validators.UserStoryStatusValidator
permission_classes = (permissions.UserStoryStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
@@ -500,11 +492,29 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
move_on_destroy_project_default_field = "default_us_status"
+class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
+ ModelCrudViewSet, BulkUpdateOrderMixin):
+
+ model = models.Points
+ serializer_class = serializers.PointsSerializer
+ validator_class = validators.PointsValidator
+ permission_classes = (permissions.PointsPermission,)
+ filter_backends = (filters.CanViewProjectFilterBackend,)
+ filter_fields = ('project',)
+ bulk_update_param = "bulk_points"
+ bulk_update_perm = "change_points"
+ bulk_update_order_action = services.bulk_update_points_order
+ move_on_destroy_related_class = RolePoints
+ move_on_destroy_related_field = "points"
+ move_on_destroy_project_default_field = "default_points"
+
+
class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.TaskStatus
serializer_class = serializers.TaskStatusSerializer
+ validator_class = validators.TaskStatusValidator
permission_classes = (permissions.TaskStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -521,6 +531,7 @@ class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.Severity
serializer_class = serializers.SeveritySerializer
+ validator_class = validators.SeverityValidator
permission_classes = (permissions.SeverityPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -536,6 +547,7 @@ class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.Priority
serializer_class = serializers.PrioritySerializer
+ validator_class = validators.PriorityValidator
permission_classes = (permissions.PriorityPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -551,6 +563,7 @@ class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueType
serializer_class = serializers.IssueTypeSerializer
+ validator_class = validators.IssueTypeValidator
permission_classes = (permissions.IssueTypePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -566,6 +579,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueStatus
serializer_class = serializers.IssueStatusSerializer
+ validator_class = validators.IssueStatusValidator
permission_classes = (permissions.IssueStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -584,6 +598,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
class ProjectTemplateViewSet(ModelCrudViewSet):
model = models.ProjectTemplate
serializer_class = serializers.ProjectTemplateSerializer
+ validator_class = validators.ProjectTemplateValidator
permission_classes = (permissions.ProjectTemplatePermission,)
def get_queryset(self):
@@ -597,7 +612,9 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Membership
admin_serializer_class = serializers.MembershipAdminSerializer
+ admin_validator_class = validators.MembershipAdminValidator
serializer_class = serializers.MembershipSerializer
+ validator_class = validators.MembershipValidator
permission_classes = (permissions.MembershipPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project", "role")
@@ -609,12 +626,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
use_admin_serializer = True
if self.action == "retrieve":
- use_admin_serializer = permissions_service.is_project_admin(self.request.user, self.object.project)
+ use_admin_serializer = permissions_services.is_project_admin(self.request.user, self.object.project)
project_id = self.request.QUERY_PARAMS.get("project", None)
if self.action == "list" and project_id is not None:
project = get_object_or_404(models.Project, pk=project_id)
- use_admin_serializer = permissions_service.is_project_admin(self.request.user, project)
+ use_admin_serializer = permissions_services.is_project_admin(self.request.user, project)
if use_admin_serializer:
return self.admin_serializer_class
@@ -622,6 +639,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
else:
return self.serializer_class
+ def get_validator_class(self):
+ if self.action == "create":
+ return self.admin_validator_class
+
+ return self.validator_class
+
def _check_if_project_can_have_more_memberships(self, project, total_new_memberships):
(can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships(
project,
@@ -636,11 +659,11 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.MembersBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.MembersBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = models.Project.objects.get(id=data["project_id"])
invitation_extra_text = data.get("invitation_extra_text", None)
self.check_permissions(request, 'bulk_create', project)
@@ -657,7 +680,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
invitation_extra_text=invitation_extra_text,
callback=self.post_save,
precall=self.pre_save)
- except ValidationError as err:
+ except exc.ValidationError as err:
return response.BadRequest(err.message_dict)
members_serialized = self.admin_serializer_class(members, many=True)
diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py
index a390b5f5..634d56ce 100644
--- a/taiga/projects/apps.py
+++ b/taiga/projects/apps.py
@@ -25,18 +25,16 @@ from django.db.models import signals
def connect_projects_signals():
from . import signals as handlers
+ from .tagging import signals as tagging_handlers
# On project object is created apply template.
signals.post_save.connect(handlers.project_post_save,
sender=apps.get_model("projects", "Project"),
dispatch_uid='project_post_save')
# Tags normalization after save a project
- signals.pre_save.connect(handlers.tags_normalization,
+ signals.pre_save.connect(tagging_handlers.tags_normalization,
sender=apps.get_model("projects", "Project"),
dispatch_uid="tags_normalization_projects")
- signals.pre_save.connect(handlers.update_project_tags_when_create_or_edit_taggable_item,
- sender=apps.get_model("projects", "Project"),
- dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects")
def disconnect_projects_signals():
@@ -44,8 +42,6 @@ def disconnect_projects_signals():
dispatch_uid='project_post_save')
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
dispatch_uid="tags_normalization_projects")
- signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
- dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects")
## Memberships Signals
diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py
index f7b223e2..3bcbf6cf 100644
--- a/taiga/projects/attachments/api.py
+++ b/taiga/projects/attachments/api.py
@@ -34,6 +34,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from . import permissions
from . import serializers
+from . import validators
from . import models
@@ -42,6 +43,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
model = models.Attachment
serializer_class = serializers.AttachmentSerializer
+ validator_class = validators.AttachmentValidator
filter_fields = ["project", "object_id"]
content_type = None
@@ -63,6 +65,9 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
obj.size = obj.attached_file.size
obj.name = path.basename(obj.attached_file.name)
+ if obj.content_object is None:
+ raise exc.WrongArguments(_("Object id issue isn't exists"))
+
if obj.project_id != obj.content_object.project_id:
raise exc.WrongArguments(_("Project ID not matches between object and project"))
@@ -72,12 +77,18 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
# NOTE: When destroy an attachment, the content_object change
# after and not before
self.persist_history_snapshot(obj, delete=True)
- super().pre_delete(obj)
+ super().post_delete(obj)
def get_object_for_snapshot(self, obj):
return obj.content_object
+class EpicAttachmentViewSet(BaseAttachmentViewSet):
+ permission_classes = (permissions.EpicAttachmentPermission,)
+ filter_backends = (filters.CanViewEpicAttachmentFilterBackend,)
+ content_type = "epics.epic"
+
+
class UserStoryAttachmentViewSet(BaseAttachmentViewSet):
permission_classes = (permissions.UserStoryAttachmentPermission,)
filter_backends = (filters.CanViewUserStoryAttachmentFilterBackend,)
diff --git a/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py b/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py
new file mode 100644
index 00000000..ee291a9f
--- /dev/null
+++ b/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-17 12:33
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0005_attachment_sha1'),
+ ]
+
+ operations = [
+ migrations.AlterIndexTogether(
+ name='attachment',
+ index_together=set([('content_type', 'object_id')]),
+ ),
+ ]
diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py
index 8bbbee16..a5110a4b 100644
--- a/taiga/projects/attachments/models.py
+++ b/taiga/projects/attachments/models.py
@@ -70,6 +70,7 @@ class Attachment(models.Model):
permissions = (
("view_attachment", "Can view attachment"),
)
+ index_together = [("content_type", "object_id")]
def __init__(self, *args, **kwargs):
super(Attachment, self).__init__(*args, **kwargs)
diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py
index 4e0f5d3e..4c7a7915 100644
--- a/taiga/projects/attachments/permissions.py
+++ b/taiga/projects/attachments/permissions.py
@@ -28,6 +28,15 @@ class IsAttachmentOwnerPerm(PermissionComponent):
return False
+class EpicAttachmentPermission(TaigaResourcePermission):
+ retrieve_perms = HasProjectPerm('view_epics') | IsAttachmentOwnerPerm()
+ create_perms = HasProjectPerm('modify_epic')
+ update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm()
+ partial_update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm()
+ destroy_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm()
+ list_perms = AllowAny()
+
+
class UserStoryAttachmentPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_us') | IsAttachmentOwnerPerm()
create_perms = HasProjectPerm('modify_us')
@@ -67,7 +76,9 @@ class WikiAttachmentPermission(TaigaResourcePermission):
class RawAttachmentPerm(PermissionComponent):
def check_permissions(self, request, view, obj=None):
is_owner = IsAttachmentOwnerPerm().check_permissions(request, view, obj)
- if obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory":
+ if obj.content_type.app_label == "epics" and obj.content_type.model == "epic":
+ return EpicAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner
+ elif obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory":
return UserStoryAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner
elif obj.content_type.app_label == "tasks" and obj.content_type.model == "task":
return TaskAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner
diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py
index 904498a9..ce8893b7 100644
--- a/taiga/projects/attachments/serializers.py
+++ b/taiga/projects/attachments/serializers.py
@@ -16,26 +16,60 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.conf import settings
+
from taiga.base.api import serializers
+from taiga.base.fields import MethodField, Field, FileField
+from taiga.base.utils.thumbnails import get_thumbnail_url
from . import services
-from . import models
-class AttachmentSerializer(serializers.ModelSerializer):
- url = serializers.SerializerMethodField("get_url")
- thumbnail_card_url = serializers.SerializerMethodField("get_thumbnail_card_url")
- attached_file = serializers.FileField(required=True)
-
- class Meta:
- model = models.Attachment
- fields = ("id", "project", "owner", "name", "attached_file", "size",
- "url", "thumbnail_card_url", "description", "is_deprecated",
- "created_date", "modified_date", "object_id", "order", "sha1")
- read_only_fields = ("owner", "created_date", "modified_date", "sha1")
+class AttachmentSerializer(serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ owner = Field(attr="owner_id")
+ name = Field()
+ attached_file = FileField()
+ size = Field()
+ url = Field()
+ description = Field()
+ is_deprecated = Field()
+ created_date = Field()
+ modified_date = Field()
+ object_id = Field()
+ order = Field()
+ sha1 = Field()
+ url = MethodField("get_url")
+ thumbnail_card_url = MethodField("get_thumbnail_card_url")
def get_url(self, obj):
return obj.attached_file.url
def get_thumbnail_card_url(self, obj):
return services.get_card_image_thumbnail_url(obj)
+
+
+class BasicAttachmentsInfoSerializerMixin(serializers.LightSerializer):
+ """
+ Assumptions:
+ - The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information
+ about the related elements, otherwise it will be empty
+ - The method attach_basic_attachments has been used to include the necessary
+ json data about the attachments in the "attachments_attr" column
+ """
+ attachments = MethodField()
+
+ def get_attachments(self, obj):
+ include_attachments = getattr(obj, "include_attachments", False)
+
+ if include_attachments:
+ assert hasattr(obj, "attachments_attr"), "instance must have a attachments_attr attribute"
+
+ if not include_attachments or obj.attachments_attr is None:
+ return []
+
+ for at in obj.attachments_attr:
+ at["thumbnail_card_url"] = get_thumbnail_url(at["attached_file"], settings.THN_ATTACHMENT_CARD)
+
+ return obj.attachments_attr
diff --git a/taiga/projects/attachments/utils.py b/taiga/projects/attachments/utils.py
new file mode 100644
index 00000000..5103fccb
--- /dev/null
+++ b/taiga/projects/attachments/utils.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.apps import apps
+
+def attach_basic_attachments(queryset, as_field="attachments_attr"):
+ """Attach basic attachments info as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the role points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
+
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ attachments_attachment.id,
+ attachments_attachment.attached_file
+ FROM attachments_attachment
+ WHERE attachments_attachment.object_id = {tbl}.id AND attachments_attachment.content_type_id = {type_id}
+ ORDER BY attachments_attachment.order, attachments_attachment.id) t"""
+
+ sql = sql.format(tbl=model._meta.db_table, type_id=type.id)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
diff --git a/taiga/projects/attachments/validators.py b/taiga/projects/attachments/validators.py
new file mode 100644
index 00000000..72355ce4
--- /dev/null
+++ b/taiga/projects/attachments/validators.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+
+from . import models
+
+
+class AttachmentValidator(validators.ModelValidator):
+ attached_file = serializers.FileField(required=True)
+
+ class Meta:
+ model = models.Attachment
+ fields = ("id", "project", "owner", "name", "attached_file", "size",
+ "description", "is_deprecated", "created_date",
+ "modified_date", "object_id", "order", "sha1")
+ read_only_fields = ("owner", "created_date", "modified_date", "sha1")
diff --git a/taiga/projects/custom_attributes/admin.py b/taiga/projects/custom_attributes/admin.py
index fca94b96..ffa676d5 100644
--- a/taiga/projects/custom_attributes/admin.py
+++ b/taiga/projects/custom_attributes/admin.py
@@ -38,6 +38,11 @@ class BaseCustomAttributeAdmin:
raw_id_fields = ["project"]
+@admin.register(models.EpicCustomAttribute)
+class EpicCustomAttributeAdmin(BaseCustomAttributeAdmin, admin.ModelAdmin):
+ pass
+
+
@admin.register(models.UserStoryCustomAttribute)
class UserStoryCustomAttributeAdmin(BaseCustomAttributeAdmin, admin.ModelAdmin):
pass
diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py
index 9bfc774f..f8e74b00 100644
--- a/taiga/projects/custom_attributes/api.py
+++ b/taiga/projects/custom_attributes/api.py
@@ -32,6 +32,7 @@ from taiga.projects.occ.mixins import OCCResourceMixin
from . import models
from . import serializers
+from . import validators
from . import permissions
from . import services
@@ -40,9 +41,22 @@ from . import services
# Custom Attribute ViewSets
#######################################################
+class EpicCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
+ model = models.EpicCustomAttribute
+ serializer_class = serializers.EpicCustomAttributeSerializer
+ validator_class = validators.EpicCustomAttributeValidator
+ permission_classes = (permissions.EpicCustomAttributePermission,)
+ filter_backends = (filters.CanViewProjectFilterBackend,)
+ filter_fields = ("project",)
+ bulk_update_param = "bulk_epic_custom_attributes"
+ bulk_update_perm = "change_epic_custom_attributes"
+ bulk_update_order_action = services.bulk_update_epic_custom_attribute_order
+
+
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.UserStoryCustomAttribute
serializer_class = serializers.UserStoryCustomAttributeSerializer
+ validator_class = validators.UserStoryCustomAttributeValidator
permission_classes = (permissions.UserStoryCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -54,6 +68,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixi
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.TaskCustomAttribute
serializer_class = serializers.TaskCustomAttributeSerializer
+ validator_class = validators.TaskCustomAttributeValidator
permission_classes = (permissions.TaskCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -65,6 +80,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, Mo
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.IssueCustomAttribute
serializer_class = serializers.IssueCustomAttributeSerializer
+ validator_class = validators.IssueCustomAttributeValidator
permission_classes = (permissions.IssueCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -83,9 +99,24 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin,
return getattr(obj, self.content_object)
+class EpicCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
+ model = models.EpicCustomAttributesValues
+ serializer_class = serializers.EpicCustomAttributesValuesSerializer
+ validator_class = validators.EpicCustomAttributesValuesValidator
+ permission_classes = (permissions.EpicCustomAttributesValuesPermission,)
+ lookup_field = "epic_id"
+ content_object = "epic"
+
+ def get_queryset(self):
+ qs = self.model.objects.all()
+ qs = qs.select_related("epic", "epic__project")
+ return qs
+
+
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.UserStoryCustomAttributesValues
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer
+ validator_class = validators.UserStoryCustomAttributesValuesValidator
permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,)
lookup_field = "user_story_id"
content_object = "user_story"
@@ -99,6 +130,7 @@ class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.TaskCustomAttributesValues
serializer_class = serializers.TaskCustomAttributesValuesSerializer
+ validator_class = validators.TaskCustomAttributesValuesValidator
permission_classes = (permissions.TaskCustomAttributesValuesPermission,)
lookup_field = "task_id"
content_object = "task"
@@ -112,6 +144,7 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.IssueCustomAttributesValues
serializer_class = serializers.IssueCustomAttributesValuesSerializer
+ validator_class = validators.IssueCustomAttributesValuesValidator
permission_classes = (permissions.IssueCustomAttributesValuesPermission,)
lookup_field = "issue_id"
content_object = "issue"
diff --git a/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py b/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py
index 6f2d86f7..4c0509bb 100644
--- a/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py
+++ b/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py
@@ -15,50 +15,50 @@ class Migration(migrations.Migration):
# Function: Remove a key in a json field
migrations.RunSQL(
"""
- CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[])
- RETURNS json
- LANGUAGE sql
- IMMUTABLE
- STRICT
- AS $function$
- SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}')
- FROM json_each("json")
- WHERE "key" <> ALL ("keys_to_delete")),
- '{}')::json $function$;
+ CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[])
+ RETURNS json
+ LANGUAGE sql
+ IMMUTABLE
+ STRICT
+ AS $function$
+ SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}')
+ FROM json_each("json")
+ WHERE "key" <> ALL ("keys_to_delete")),
+ '{}')::json $function$;
""",
- reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[])
- CASCADE;"""
+ reverse_sql="""
+ DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[])
+ CASCADE;"""
),
# Function: Romeve a key in the json field of *_custom_attributes_values.values
migrations.RunSQL(
"""
- CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"()
- RETURNS trigger
- AS $clean_key_in_custom_attributes_values$
- DECLARE
- key text;
- project_id int;
- object_id int;
- attribute text;
- tablename text;
- custom_attributes_tablename text;
- BEGIN
- key := OLD.id::text;
- project_id := OLD.project_id;
- attribute := TG_ARGV[0]::text;
- tablename := TG_ARGV[1]::text;
- custom_attributes_tablename := TG_ARGV[2]::text;
-
- EXECUTE 'UPDATE ' || quote_ident(custom_attributes_tablename) || '
- SET attributes_values = json_object_delete_keys(attributes_values, ' || quote_literal(key) || ')
- FROM ' || quote_ident(tablename) || '
- WHERE ' || quote_ident(tablename) || '.project_id = ' || project_id || '
- AND ' || quote_ident(custom_attributes_tablename) || '.' || quote_ident(attribute) || ' = ' || quote_ident(tablename) || '.id';
- RETURN NULL;
- END; $clean_key_in_custom_attributes_values$
- LANGUAGE plpgsql;
+ CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"()
+ RETURNS trigger
+ AS $clean_key_in_custom_attributes_values$
+ DECLARE
+ key text;
+ project_id int;
+ object_id int;
+ attribute text;
+ tablename text;
+ custom_attributes_tablename text;
+ BEGIN
+ key := OLD.id::text;
+ project_id := OLD.project_id;
+ attribute := TG_ARGV[0]::text;
+ tablename := TG_ARGV[1]::text;
+ custom_attributes_tablename := TG_ARGV[2]::text;
+ EXECUTE 'UPDATE ' || quote_ident(custom_attributes_tablename) || '
+ SET attributes_values = json_object_delete_keys(attributes_values, ' || quote_literal(key) || ')
+ FROM ' || quote_ident(tablename) || '
+ WHERE ' || quote_ident(tablename) || '.project_id = ' || project_id || '
+ AND ' || quote_ident(custom_attributes_tablename) || '.' || quote_ident(attribute) || ' = ' || quote_ident(tablename) || '.id';
+ RETURN NULL;
+ END; $clean_key_in_custom_attributes_values$
+ LANGUAGE plpgsql;
"""
),
@@ -66,13 +66,14 @@ class Migration(migrations.Migration):
migrations.RunSQL(
"""
DROP TRIGGER IF EXISTS "update_userstorycustomvalues_after_remove_userstorycustomattribute"
- ON custom_attributes_userstorycustomattribute
- CASCADE;
+ ON custom_attributes_userstorycustomattribute
+ CASCADE;
CREATE TRIGGER "update_userstorycustomvalues_after_remove_userstorycustomattribute"
AFTER DELETE ON custom_attributes_userstorycustomattribute
FOR EACH ROW
- EXECUTE PROCEDURE clean_key_in_custom_attributes_values('user_story_id', 'userstories_userstory', 'custom_attributes_userstorycustomattributesvalues');
+ EXECUTE PROCEDURE clean_key_in_custom_attributes_values('user_story_id', 'userstories_userstory',
+ 'custom_attributes_userstorycustomattributesvalues');
"""
),
@@ -80,13 +81,14 @@ class Migration(migrations.Migration):
migrations.RunSQL(
"""
DROP TRIGGER IF EXISTS "update_taskcustomvalues_after_remove_taskcustomattribute"
- ON custom_attributes_taskcustomattribute
- CASCADE;
+ ON custom_attributes_taskcustomattribute
+ CASCADE;
CREATE TRIGGER "update_taskcustomvalues_after_remove_taskcustomattribute"
AFTER DELETE ON custom_attributes_taskcustomattribute
FOR EACH ROW
- EXECUTE PROCEDURE clean_key_in_custom_attributes_values('task_id', 'tasks_task', 'custom_attributes_taskcustomattributesvalues');
+ EXECUTE PROCEDURE clean_key_in_custom_attributes_values('task_id', 'tasks_task',
+ 'custom_attributes_taskcustomattributesvalues');
"""
),
@@ -94,13 +96,14 @@ class Migration(migrations.Migration):
migrations.RunSQL(
"""
DROP TRIGGER IF EXISTS "update_issuecustomvalues_after_remove_issuecustomattribute"
- ON custom_attributes_issuecustomattribute
- CASCADE;
+ ON custom_attributes_issuecustomattribute
+ CASCADE;
CREATE TRIGGER "update_issuecustomvalues_after_remove_issuecustomattribute"
AFTER DELETE ON custom_attributes_issuecustomattribute
FOR EACH ROW
- EXECUTE PROCEDURE clean_key_in_custom_attributes_values('issue_id', 'issues_issue', 'custom_attributes_issuecustomattributesvalues');
+ EXECUTE PROCEDURE clean_key_in_custom_attributes_values('issue_id', 'issues_issue',
+ 'custom_attributes_issuecustomattributesvalues');
"""
),
migrations.AlterIndexTogether(
diff --git a/taiga/projects/custom_attributes/migrations/0009_auto_20160728_1002.py b/taiga/projects/custom_attributes/migrations/0009_auto_20160728_1002.py
new file mode 100644
index 00000000..313e22fd
--- /dev/null
+++ b/taiga/projects/custom_attributes/migrations/0009_auto_20160728_1002.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-28 10:02
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import django_pgjson.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('epics', '0002_epic_color'),
+ ('projects', '0050_project_epics_csv_uuid'),
+ ('custom_attributes', '0008_auto_20160728_0540'),
+ ]
+
+ operations = [
+ # Change some verbose names
+ migrations.AlterModelOptions(
+ name='issuecustomattributesvalues',
+ options={'ordering': ['id'], 'verbose_name': 'issue custom attributes values', 'verbose_name_plural': 'issue custom attributes values'},
+ ),
+ migrations.AlterModelOptions(
+ name='taskcustomattributesvalues',
+ options={'ordering': ['id'], 'verbose_name': 'task custom attributes values', 'verbose_name_plural': 'task custom attributes values'},
+ ),
+ migrations.AlterModelOptions(
+ name='userstorycustomattributesvalues',
+ options={'ordering': ['id'], 'verbose_name': 'user story custom attributes values', 'verbose_name_plural': 'user story custom attributes values'},
+ ),
+ # Custom attributes for epics
+ migrations.CreateModel(
+ name='EpicCustomAttribute',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=64, verbose_name='name')),
+ ('description', models.TextField(blank=True, verbose_name='description')),
+ ('type', models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')], default='text', max_length=16, verbose_name='type')),
+ ('order', models.IntegerField(default=10000, verbose_name='order')),
+ ('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')),
+ ('modified_date', models.DateTimeField(verbose_name='modified date')),
+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epiccustomattributes', to='projects.Project', verbose_name='project')),
+ ],
+ options={
+ 'verbose_name': 'epic custom attribute',
+ 'abstract': False,
+ 'ordering': ['project', 'order', 'name'],
+ 'verbose_name_plural': 'epic custom attributes',
+ },
+ ),
+ migrations.CreateModel(
+ name='EpicCustomAttributesValues',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('version', models.IntegerField(default=1, verbose_name='version')),
+ ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='values')),
+ ('epic', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_attributes_values', to='epics.Epic', verbose_name='epic')),
+ ],
+ options={
+ 'abstract': False,
+ 'verbose_name': 'epic custom attributes values',
+ 'ordering': ['id'],
+ 'verbose_name_plural': 'epic custom attributes values',
+ },
+ ),
+ migrations.AlterIndexTogether(
+ name='epiccustomattributesvalues',
+ index_together=set([('epic',)]),
+ ),
+ migrations.AlterUniqueTogether(
+ name='epiccustomattribute',
+ unique_together=set([('project', 'name')]),
+ ),
+ migrations.RunSQL(
+ """
+ CREATE TRIGGER "update_epiccustomvalues_after_remove_epiccustomattribute"
+ AFTER DELETE ON custom_attributes_epiccustomattribute
+ FOR EACH ROW
+ EXECUTE PROCEDURE clean_key_in_custom_attributes_values('epic_id', 'epics_epic',
+ 'custom_attributes_epiccustomattributesvalues');
+ """,
+ reverse_sql="""DROP TRIGGER IF EXISTS "update_epiccustomvalues_after_remove_epiccustomattribute"
+ ON custom_attributes_epiccustomattribute
+ CASCADE;"""
+ ),
+ ]
diff --git a/taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py b/taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py
new file mode 100644
index 00000000..afe2277a
--- /dev/null
+++ b/taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-28 05:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.time
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('custom_attributes', '0009_auto_20160728_1002'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='epiccustomattribute',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='issuecustomattribute',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='taskcustomattribute',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'),
+ ),
+ migrations.AlterField(
+ model_name='userstorycustomattribute',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'),
+ ),
+ ]
diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py
index 5fe3c6a0..6467f97e 100644
--- a/taiga/projects/custom_attributes/models.py
+++ b/taiga/projects/custom_attributes/models.py
@@ -22,6 +22,7 @@ from django.utils import timezone
from django_pgjson.fields import JsonField
+from taiga.base.utils.time import timestamp_ms
from taiga.projects.occ.mixins import OCCModelMixin
from . import choices
@@ -31,14 +32,13 @@ from . import choices
# Custom Attribute Models
#######################################################
-
class AbstractCustomAttribute(models.Model):
name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name"))
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
type = models.CharField(null=False, blank=False, max_length=16,
choices=choices.TYPES_CHOICES, default=choices.TEXT_TYPE,
verbose_name=_("type"))
- order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("order"))
+ order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order"))
project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss",
verbose_name=_("project"))
@@ -63,6 +63,12 @@ class AbstractCustomAttribute(models.Model):
return super().save(*args, **kwargs)
+class EpicCustomAttribute(AbstractCustomAttribute):
+ class Meta(AbstractCustomAttribute.Meta):
+ verbose_name = "epic custom attribute"
+ verbose_name_plural = "epic custom attributes"
+
+
class UserStoryCustomAttribute(AbstractCustomAttribute):
class Meta(AbstractCustomAttribute.Meta):
verbose_name = "user story custom attribute"
@@ -93,13 +99,29 @@ class AbstractCustomAttributesValues(OCCModelMixin, models.Model):
ordering = ["id"]
+class EpicCustomAttributesValues(AbstractCustomAttributesValues):
+ epic = models.OneToOneField("epics.Epic",
+ null=False, blank=False, related_name="custom_attributes_values",
+ verbose_name=_("epic"))
+
+ class Meta(AbstractCustomAttributesValues.Meta):
+ verbose_name = "epic custom attributes values"
+ verbose_name_plural = "epic custom attributes values"
+ index_together = [("epic",)]
+
+ @property
+ def project(self):
+ # NOTE: This property simplifies checking permissions
+ return self.epic.project
+
+
class UserStoryCustomAttributesValues(AbstractCustomAttributesValues):
user_story = models.OneToOneField("userstories.UserStory",
null=False, blank=False, related_name="custom_attributes_values",
verbose_name=_("user story"))
class Meta(AbstractCustomAttributesValues.Meta):
- verbose_name = "user story ustom attributes values"
+ verbose_name = "user story custom attributes values"
verbose_name_plural = "user story custom attributes values"
index_together = [("user_story",)]
@@ -115,7 +137,7 @@ class TaskCustomAttributesValues(AbstractCustomAttributesValues):
verbose_name=_("task"))
class Meta(AbstractCustomAttributesValues.Meta):
- verbose_name = "task ustom attributes values"
+ verbose_name = "task custom attributes values"
verbose_name_plural = "task custom attributes values"
index_together = [("task",)]
@@ -131,7 +153,7 @@ class IssueCustomAttributesValues(AbstractCustomAttributesValues):
verbose_name=_("issue"))
class Meta(AbstractCustomAttributesValues.Meta):
- verbose_name = "issue ustom attributes values"
+ verbose_name = "issue custom attributes values"
verbose_name_plural = "issue custom attributes values"
index_together = [("issue",)]
diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py
index 5771cce4..ffc6a04c 100644
--- a/taiga/projects/custom_attributes/permissions.py
+++ b/taiga/projects/custom_attributes/permissions.py
@@ -27,6 +27,18 @@ from taiga.base.api.permissions import IsSuperUser
# Custom Attribute Permissions
#######################################################
+class EpicCustomAttributePermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_project')
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
+ list_perms = AllowAny()
+ bulk_update_order_perms = IsProjectAdmin()
+
+
class UserStoryCustomAttributePermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
@@ -67,6 +79,14 @@ class IssueCustomAttributePermission(TaigaResourcePermission):
# Custom Attributes Values Permissions
#######################################################
+class EpicCustomAttributesValuesPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_us')
+ update_perms = HasProjectPerm('modify_us')
+ partial_update_perms = HasProjectPerm('modify_us')
+
+
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py
index 64a934f5..10e9c756 100644
--- a/taiga/projects/custom_attributes/serializers.py
+++ b/taiga/projects/custom_attributes/serializers.py
@@ -17,131 +17,60 @@
# along with this program. If not, see .
-from django.apps import apps
-from django.utils.translation import ugettext_lazy as _
-
-from taiga.base.fields import JsonField
-from taiga.base.api.serializers import ValidationError
-from taiga.base.api.serializers import ModelSerializer
-
-from . import models
+from taiga.base.fields import JsonField, Field
+from taiga.base.api import serializers
######################################################
# Custom Attribute Serializer
#######################################################
-class BaseCustomAttributeSerializer(ModelSerializer):
- class Meta:
- read_only_fields = ('id',)
- exclude = ('created_date', 'modified_date')
+class BaseCustomAttributeSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ description = Field()
+ type = Field()
+ order = Field()
+ project = Field(attr="project_id")
+ created_date = Field()
+ modified_date = Field()
- def _validate_integrity_between_project_and_name(self, attrs, source):
- """
- Check the name is not duplicated in the project. Check when:
- - create a new one
- - update the name
- - update the project (move to another project)
- """
- data_id = attrs.get("id", None)
- data_name = attrs.get("name", None)
- data_project = attrs.get("project", None)
- if self.object:
- data_id = data_id or self.object.id
- data_name = data_name or self.object.name
- data_project = data_project or self.object.project
-
- model = self.Meta.model
- qs = (model.objects.filter(project=data_project, name=data_name)
- .exclude(id=data_id))
- if qs.exists():
- raise ValidationError(_("Already exists one with the same name."))
-
- return attrs
-
- def validate_name(self, attrs, source):
- return self._validate_integrity_between_project_and_name(attrs, source)
-
- def validate_project(self, attrs, source):
- return self._validate_integrity_between_project_and_name(attrs, source)
+class EpicCustomAttributeSerializer(BaseCustomAttributeSerializer):
+ pass
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
- class Meta(BaseCustomAttributeSerializer.Meta):
- model = models.UserStoryCustomAttribute
+ pass
class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer):
- class Meta(BaseCustomAttributeSerializer.Meta):
- model = models.TaskCustomAttribute
+ pass
class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer):
- class Meta(BaseCustomAttributeSerializer.Meta):
- model = models.IssueCustomAttribute
+ pass
######################################################
# Custom Attribute Serializer
#######################################################
+class BaseCustomAttributesValuesSerializer(serializers.LightSerializer):
+ attributes_values = Field()
+ version = Field()
-class BaseCustomAttributesValuesSerializer(ModelSerializer):
- attributes_values = JsonField(source="attributes_values", label="attributes values")
- _custom_attribute_model = None
- _container_field = None
-
- class Meta:
- exclude = ("id",)
-
- def validate_attributes_values(self, attrs, source):
- # values must be a dict
- data_values = attrs.get("attributes_values", None)
- if self.object:
- data_values = (data_values or self.object.attributes_values)
-
- if type(data_values) is not dict:
- raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
-
- # Values keys must be in the container object project
- data_container = attrs.get(self._container_field, None)
- if data_container:
- project_id = data_container.project_id
- elif self.object:
- project_id = getattr(self.object, self._container_field).project_id
- else:
- project_id = None
-
- values_ids = list(data_values.keys())
- qs = self._custom_attribute_model.objects.filter(project=project_id,
- id__in=values_ids)
- if qs.count() != len(values_ids):
- raise ValidationError(_("It contain invalid custom fields."))
-
- return attrs
+class EpicCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
+ epic = Field(attr="epic.id")
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
- _custom_attribute_model = models.UserStoryCustomAttribute
- _container_model = "userstories.UserStory"
- _container_field = "user_story"
-
- class Meta(BaseCustomAttributesValuesSerializer.Meta):
- model = models.UserStoryCustomAttributesValues
+ user_story = Field(attr="user_story.id")
-class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
- _custom_attribute_model = models.TaskCustomAttribute
- _container_field = "task"
-
- class Meta(BaseCustomAttributesValuesSerializer.Meta):
- model = models.TaskCustomAttributesValues
+class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
+ task = Field(attr="task.id")
-class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
- _custom_attribute_model = models.IssueCustomAttribute
- _container_field = "issue"
-
- class Meta(BaseCustomAttributesValuesSerializer.Meta):
- model = models.IssueCustomAttributesValues
+class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
+ issue = Field(attr="issue.id")
diff --git a/taiga/projects/custom_attributes/services.py b/taiga/projects/custom_attributes/services.py
index c957c5dc..4a30305e 100644
--- a/taiga/projects/custom_attributes/services.py
+++ b/taiga/projects/custom_attributes/services.py
@@ -20,6 +20,23 @@ from django.db import transaction
from django.db import connection
+@transaction.atomic
+def bulk_update_epic_custom_attribute_order(project, user, data):
+ cursor = connection.cursor()
+
+ sql = """
+ prepare bulk_update_order as update custom_attributes_epiccustomattribute set "order" = $1
+ where custom_attributes_epiccustomattribute.id = $2 and
+ custom_attributes_epiccustomattribute.project_id = $3;
+ """
+ cursor.execute(sql)
+ for id, order in data:
+ cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);",
+ (order, id, project.id))
+ cursor.execute("DEALLOCATE bulk_update_order")
+ cursor.close()
+
+
@transaction.atomic
def bulk_update_userstory_custom_attribute_order(project, user, data):
cursor = connection.cursor()
diff --git a/taiga/projects/custom_attributes/signals.py b/taiga/projects/custom_attributes/signals.py
index 72b715a7..96e74e9e 100644
--- a/taiga/projects/custom_attributes/signals.py
+++ b/taiga/projects/custom_attributes/signals.py
@@ -19,6 +19,12 @@
from . import models
+def create_custom_attribute_value_when_create_epic(sender, instance, created, **kwargs):
+ if created:
+ models.EpicCustomAttributesValues.objects.get_or_create(epic=instance,
+ defaults={"attributes_values":{}})
+
+
def create_custom_attribute_value_when_create_user_story(sender, instance, created, **kwargs):
if created:
models.UserStoryCustomAttributesValues.objects.get_or_create(user_story=instance,
diff --git a/taiga/projects/custom_attributes/validators.py b/taiga/projects/custom_attributes/validators.py
new file mode 100644
index 00000000..4169eee6
--- /dev/null
+++ b/taiga/projects/custom_attributes/validators.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+
+from django.utils.translation import ugettext_lazy as _
+
+from taiga.base.fields import JsonField
+from taiga.base.exceptions import ValidationError
+from taiga.base.api.validators import ModelValidator
+
+from . import models
+
+
+######################################################
+# Custom Attribute Validator
+#######################################################
+
+class BaseCustomAttributeValidator(ModelValidator):
+ class Meta:
+ read_only_fields = ('id',)
+ exclude = ('created_date', 'modified_date')
+
+ def _validate_integrity_between_project_and_name(self, attrs, source):
+ """
+ Check the name is not duplicated in the project. Check when:
+ - create a new one
+ - update the name
+ - update the project (move to another project)
+ """
+ data_id = attrs.get("id", None)
+ data_name = attrs.get("name", None)
+ data_project = attrs.get("project", None)
+
+ if self.object:
+ data_id = data_id or self.object.id
+ data_name = data_name or self.object.name
+ data_project = data_project or self.object.project
+
+ model = self.Meta.model
+ qs = (model.objects.filter(project=data_project, name=data_name)
+ .exclude(id=data_id))
+ if qs.exists():
+ raise ValidationError(_("Already exists one with the same name."))
+
+ return attrs
+
+ def validate_name(self, attrs, source):
+ return self._validate_integrity_between_project_and_name(attrs, source)
+
+ def validate_project(self, attrs, source):
+ return self._validate_integrity_between_project_and_name(attrs, source)
+
+
+class EpicCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.EpicCustomAttribute
+
+
+class UserStoryCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.UserStoryCustomAttribute
+
+
+class TaskCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.TaskCustomAttribute
+
+
+class IssueCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.IssueCustomAttribute
+
+
+######################################################
+# Custom Attribute Validator
+#######################################################
+
+
+class BaseCustomAttributesValuesValidator(ModelValidator):
+ attributes_values = JsonField(source="attributes_values", label="attributes values")
+ _custom_attribute_model = None
+ _container_field = None
+
+ class Meta:
+ exclude = ("id",)
+
+ def validate_attributes_values(self, attrs, source):
+ # values must be a dict
+ data_values = attrs.get("attributes_values", None)
+ if self.object:
+ data_values = (data_values or self.object.attributes_values)
+
+ if type(data_values) is not dict:
+ raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
+
+ # Values keys must be in the container object project
+ data_container = attrs.get(self._container_field, None)
+ if data_container:
+ project_id = data_container.project_id
+ elif self.object:
+ project_id = getattr(self.object, self._container_field).project_id
+ else:
+ project_id = None
+
+ values_ids = list(data_values.keys())
+ qs = self._custom_attribute_model.objects.filter(project=project_id,
+ id__in=values_ids)
+ if qs.count() != len(values_ids):
+ raise ValidationError(_("It contain invalid custom fields."))
+
+ return attrs
+
+
+class EpicCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator):
+ _custom_attribute_model = models.EpicCustomAttribute
+ _container_model = "epics.Epic"
+ _container_field = "epic"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.EpicCustomAttributesValues
+
+
+class UserStoryCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator):
+ _custom_attribute_model = models.UserStoryCustomAttribute
+ _container_model = "userstories.UserStory"
+ _container_field = "user_story"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.UserStoryCustomAttributesValues
+
+
+class TaskCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator, ModelValidator):
+ _custom_attribute_model = models.TaskCustomAttribute
+ _container_field = "task"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.TaskCustomAttributesValues
+
+
+class IssueCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator, ModelValidator):
+ _custom_attribute_model = models.IssueCustomAttribute
+ _container_field = "issue"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.IssueCustomAttributesValues
diff --git a/taiga/projects/epics/__init__.py b/taiga/projects/epics/__init__.py
new file mode 100644
index 00000000..cc0dd3b9
--- /dev/null
+++ b/taiga/projects/epics/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+default_app_config = "taiga.projects.epics.apps.EpicsAppConfig"
+
diff --git a/taiga/projects/epics/admin.py b/taiga/projects/epics/admin.py
new file mode 100644
index 00000000..69aea806
--- /dev/null
+++ b/taiga/projects/epics/admin.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.contrib import admin
+
+from taiga.projects.notifications.admin import WatchedInline
+from taiga.projects.votes.admin import VoteInline
+
+from . import models
+
+
+class RelatedUserStoriesInline(admin.TabularInline):
+ model = models.RelatedUserStory
+ sortable_field_name = "order"
+ raw_id_fields = ["user_story", ]
+ extra = 0
+
+
+class EpicAdmin(admin.ModelAdmin):
+ list_display = ["project", "ref", "subject"]
+ list_display_links = ["ref", "subject"]
+ inlines = [WatchedInline, VoteInline, RelatedUserStoriesInline]
+ raw_id_fields = ["project"]
+ search_fields = ["subject", "description", "id", "ref"]
+
+ def get_object(self, *args, **kwargs):
+ self.obj = super().get_object(*args, **kwargs)
+ return self.obj
+
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ if (db_field.name in ["status"] and getattr(self, 'obj', None)):
+ kwargs["queryset"] = db_field.related.model.objects.filter(project=self.obj.project)
+
+ elif (db_field.name in ["owner", "assigned_to"] and getattr(self, 'obj', None)):
+ kwargs["queryset"] = db_field.related.model.objects.filter(memberships__project=self.obj.project)
+
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
+
+ def formfield_for_manytomany(self, db_field, request, **kwargs):
+ if (db_field.name in ["watchers"] and getattr(self, 'obj', None)):
+ kwargs["queryset"] = db_field.related.parent_model.objects.filter(memberships__project=self.obj.project)
+ return super().formfield_for_manytomany(db_field, request, **kwargs)
+
+
+admin.site.register(models.Epic, EpicAdmin)
diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py
new file mode 100644
index 00000000..fed57abd
--- /dev/null
+++ b/taiga/projects/epics/api.py
@@ -0,0 +1,315 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.http import HttpResponse
+from django.utils.translation import ugettext as _
+
+from taiga.base.api.utils import get_object_or_404
+from taiga.base import filters, response
+from taiga.base import exceptions as exc
+from taiga.base.decorators import list_route
+from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api.mixins import BlockedByProjectMixin
+from taiga.base.api.viewsets import NestedViewSetMixin
+from taiga.base.utils import json
+
+from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.models import Project, EpicStatus
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
+from taiga.projects.occ import OCCResourceMixin
+from taiga.projects.tagging.api import TaggedResourceMixin
+from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
+
+from . import models
+from . import permissions
+from . import serializers
+from . import services
+from . import validators
+from . import utils as epics_utils
+
+
+class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
+ WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
+ ModelCrudViewSet):
+ validator_class = validators.EpicValidator
+ queryset = models.Epic.objects.all()
+ permission_classes = (permissions.EpicPermission,)
+ filter_backends = (filters.CanViewEpicsFilterBackend,
+ filters.OwnersFilter,
+ filters.AssignedToFilter,
+ filters.StatusesFilter,
+ filters.TagsFilter,
+ filters.WatchersFilter,
+ filters.QFilter,
+ filters.CreatedDateFilter,
+ filters.ModifiedDateFilter)
+ filter_fields = ["project",
+ "project__slug",
+ "assigned_to",
+ "status__is_closed"]
+
+ def get_serializer_class(self, *args, **kwargs):
+ if self.action in ["retrieve", "by_ref"]:
+ return serializers.EpicNeighborsSerializer
+
+ if self.action == "list":
+ return serializers.EpicListSerializer
+
+ return serializers.EpicSerializer
+
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = qs.select_related("project",
+ "status",
+ "owner",
+ "assigned_to")
+
+ include_attachments = "include_attachments" in self.request.QUERY_PARAMS
+ qs = epics_utils.attach_extra_info(qs, user=self.request.user,
+ include_attachments=include_attachments)
+
+ return qs
+
+ def pre_conditions_on_save(self, obj):
+ super().pre_conditions_on_save(obj)
+
+ if obj.status and obj.status.project != obj.project:
+ raise exc.WrongArguments(_("You don't have permissions to set this status to this epic."))
+
+ """
+ Updating the epic order attribute can affect the ordering of another epics
+ This method generate a key for the epic and can be used to be compared before and after
+ saving
+ If there is any difference it means an extra ordering update must be done
+ """
+ def _epics_order_key(self, obj):
+ return "{}-{}".format(obj.project_id, obj.epics_order)
+
+ def pre_save(self, obj):
+ if not obj.id:
+ obj.owner = self.request.user
+ else:
+ self._old_epics_order_key = self._epics_order_key(self.get_object())
+
+ super().pre_save(obj)
+
+ def _reorder_if_needed(self, obj, old_order_key, order_key):
+ # Executes the extra ordering if there is a difference in the ordering keys
+ if old_order_key == order_key:
+ return {}
+
+ extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}"))
+ data = [{"epic_id": obj.id, "order": getattr(obj, "epics_order")}]
+ for id, order in extra_orders.items():
+ data.append({"epic_id": int(id), "order": order})
+
+ return services.update_epics_order_in_bulk(data, "epics_order", project=obj.project)
+
+ def post_save(self, obj, created=False):
+ if not created:
+ # Let's reorder the related stuff after edit the element
+ orders_updated = self._reorder_if_needed(obj,
+ self._old_epics_order_key,
+ self._epics_order_key(obj))
+ self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated)
+
+ super().post_save(obj, created)
+
+ def update(self, request, *args, **kwargs):
+ self.object = self.get_object_or_none()
+ project_id = request.DATA.get('project', None)
+ if project_id and self.object and self.object.project.id != project_id:
+ try:
+ new_project = Project.objects.get(pk=project_id)
+ self.check_permissions(request, "destroy", self.object)
+ self.check_permissions(request, "create", new_project)
+
+ status_id = request.DATA.get('status', None)
+ if status_id is not None:
+ try:
+ old_status = self.object.project.epic_statuses.get(pk=status_id)
+ new_status = new_project.epic_statuses.get(slug=old_status.slug)
+ request.DATA['status'] = new_status.id
+ except EpicStatus.DoesNotExist:
+ request.DATA['status'] = new_project.default_epic_status.id
+
+ except Project.DoesNotExist:
+ return response.BadRequest(_("The project doesn't exist"))
+
+ return super().update(request, *args, **kwargs)
+
+ @list_route(methods=["GET"])
+ def filters_data(self, request, *args, **kwargs):
+ project_id = request.QUERY_PARAMS.get("project", None)
+ project = get_object_or_404(Project, id=project_id)
+
+ filter_backends = self.get_filter_backends()
+ statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
+ assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
+ owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
+
+ queryset = self.get_queryset()
+ querysets = {
+ "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
+ "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
+ "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
+ "tags": self.filter_queryset(queryset)
+ }
+ return response.Ok(services.get_epics_filters_data(project, querysets))
+
+ @list_route(methods=["GET"])
+ def by_ref(self, request):
+ retrieve_kwargs = {
+ "ref": request.QUERY_PARAMS.get("ref", None)
+ }
+ project_id = request.QUERY_PARAMS.get("project", None)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
+
+ @list_route(methods=["GET"])
+ def csv(self, request):
+ uuid = request.QUERY_PARAMS.get("uuid", None)
+ if uuid is None:
+ return response.NotFound()
+
+ project = get_object_or_404(Project, epics_csv_uuid=uuid)
+ queryset = project.epics.all().order_by('ref')
+ data = services.epics_to_csv(project, queryset)
+ csv_response = HttpResponse(data.getvalue(), content_type='application/csv; charset=utf-8')
+ csv_response['Content-Disposition'] = 'attachment; filename="epics.csv"'
+ return csv_response
+
+ @list_route(methods=["POST"])
+ def bulk_create(self, request, **kwargs):
+ validator = validators.EpicsBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+ project = Project.objects.get(id=data["project_id"])
+ self.check_permissions(request, "bulk_create", project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
+ epics = services.create_epics_in_bulk(
+ data["bulk_epics"],
+ status_id=data.get("status_id") or project.default_epic_status_id,
+ project=project,
+ owner=request.user,
+ callback=self.post_save, precall=self.pre_save)
+
+ epics = self.get_queryset().filter(id__in=[i.id for i in epics])
+ for epic in epics:
+ self.persist_history_snapshot(obj=epic)
+
+ epics_serialized = self.get_serializer_class()(epics, many=True)
+
+ return response.Ok(epics_serialized.data)
+
+
+class EpicRelatedUserStoryViewSet(NestedViewSetMixin, HistoryResourceMixin,
+ BlockedByProjectMixin, ModelCrudViewSet):
+ queryset = models.RelatedUserStory.objects.all()
+ serializer_class = serializers.EpicRelatedUserStorySerializer
+ validator_class = validators.EpicRelatedUserStoryValidator
+ model = models.RelatedUserStory
+ permission_classes = (permissions.EpicRelatedUserStoryPermission,)
+ lookup_field = "user_story"
+
+ """
+ Updating the order attribute can affect the ordering of another userstories in the epic
+ This method generate a key for the userstory and can be used to be compared before and after
+ saving
+ If there is any difference it means an extra ordering update must be done
+ """
+ def _order_key(self, obj):
+ return "{}-{}".format(obj.user_story.project_id, obj.order)
+
+ def pre_save(self, obj):
+ if not obj.id:
+ obj.epic_id = self.kwargs["epic"]
+ else:
+ self._old_order_key = self._order_key(self.get_object())
+
+ super().pre_save(obj)
+
+ def _reorder_if_needed(self, obj, old_order_key, order_key):
+ # Executes the extra ordering if there is a difference in the ordering keys
+ if old_order_key == order_key:
+ return {}
+
+ extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}"))
+ data = [{"us_id": obj.user_story.id, "order": getattr(obj, "order")}]
+ for id, order in extra_orders.items():
+ data.append({"us_id": int(id), "order": order})
+
+ return services.update_epic_related_userstories_order_in_bulk(data, epic=obj.epic)
+
+ def post_save(self, obj, created=False):
+ if not created:
+ # Let's reorder the related stuff after edit the element
+ orders_updated = self._reorder_if_needed(obj,
+ self._old_order_key,
+ self._order_key(obj))
+ self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated)
+
+ super().post_save(obj, created)
+
+ @list_route(methods=["POST"])
+ def bulk_create(self, request, **kwargs):
+ validator = validators.CreateRelatedUserStoriesBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+
+ epic = get_object_or_404(models.Epic, id=kwargs["epic"])
+ project = Project.objects.get(pk=data.get('project_id'))
+
+ self.check_permissions(request, 'bulk_create', project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
+
+ related_userstories = services.create_related_userstories_in_bulk(
+ data["bulk_userstories"],
+ epic,
+ project=project,
+ owner=request.user
+ )
+
+ for related_userstory in related_userstories:
+ self.persist_history_snapshot(obj=related_userstory)
+
+ related_uss_serialized = self.get_serializer_class()(epic.relateduserstory_set.all(), many=True)
+ return response.Ok(related_uss_serialized.data)
+
+
+class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.EpicVotersPermission,)
+ resource_model = models.Epic
+
+
+class EpicWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
+ permission_classes = (permissions.EpicWatchersPermission,)
+ resource_model = models.Epic
diff --git a/taiga/projects/epics/apps.py b/taiga/projects/epics/apps.py
new file mode 100644
index 00000000..bf489ea0
--- /dev/null
+++ b/taiga/projects/epics/apps.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.apps import AppConfig
+from django.apps import apps
+from django.db.models import signals
+
+
+def connect_epics_signals():
+ from taiga.projects.tagging import signals as tagging_handlers
+
+ # Tags
+ signals.pre_save.connect(tagging_handlers.tags_normalization,
+ sender=apps.get_model("epics", "Epic"),
+ dispatch_uid="tags_normalization_epic")
+
+
+def connect_epics_custom_attributes_signals():
+ from taiga.projects.custom_attributes import signals as custom_attributes_handlers
+ signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_epic,
+ sender=apps.get_model("epics", "Epic"),
+ dispatch_uid="create_custom_attribute_value_when_create_epic")
+
+
+def connect_all_epics_signals():
+ connect_epics_signals()
+ connect_epics_custom_attributes_signals()
+
+
+def disconnect_epics_signals():
+ signals.pre_save.disconnect(sender=apps.get_model("epics", "Epic"),
+ dispatch_uid="tags_normalization")
+
+
+def disconnect_epics_custom_attributes_signals():
+ signals.post_save.disconnect(sender=apps.get_model("epics", "Epic"),
+ dispatch_uid="create_custom_attribute_value_when_create_epic")
+
+
+def disconnect_all_epics_signals():
+ disconnect_epics_signals()
+ disconnect_epics_custom_attributes_signals()
+
+
+class EpicsAppConfig(AppConfig):
+ name = "taiga.projects.epics"
+ verbose_name = "Epics"
+
+ def ready(self):
+ connect_all_epics_signals()
diff --git a/taiga/projects/epics/migrations/0001_initial.py b/taiga/projects/epics/migrations/0001_initial.py
new file mode 100644
index 00000000..e757b7be
--- /dev/null
+++ b/taiga/projects/epics/migrations/0001_initial.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-05 11:12
+from __future__ import unicode_literals
+
+from django.conf import settings
+import django.contrib.postgres.fields
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import taiga.projects.notifications.mixins
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('userstories', '0012_auto_20160614_1201'),
+ ('projects', '0049_auto_20160629_1443'),
+ ('history', '0012_auto_20160629_1036'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Epic',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags')),
+ ('version', models.IntegerField(default=1, verbose_name='version')),
+ ('is_blocked', models.BooleanField(default=False, verbose_name='is blocked')),
+ ('blocked_note', models.TextField(blank=True, default='', verbose_name='blocked note')),
+ ('ref', models.BigIntegerField(blank=True, db_index=True, default=None, null=True, verbose_name='ref')),
+ ('epics_order', models.IntegerField(default=10000, verbose_name='epics order')),
+ ('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')),
+ ('modified_date', models.DateTimeField(verbose_name='modified date')),
+ ('subject', models.TextField(verbose_name='subject')),
+ ('description', models.TextField(blank=True, verbose_name='description')),
+ ('client_requirement', models.BooleanField(default=False, verbose_name='is client requirement')),
+ ('team_requirement', models.BooleanField(default=False, verbose_name='is team requirement')),
+ ('assigned_to', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='epics_assigned_to_me', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')),
+ ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_epics', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
+ ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epics', to='projects.Project', verbose_name='project')),
+ ('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='epics', to='projects.EpicStatus', verbose_name='status')),
+ ],
+ options={
+ 'ordering': ['project', 'epics_order', 'ref'],
+ 'verbose_name_plural': 'epics',
+ 'verbose_name': 'epic',
+ },
+ bases=(taiga.projects.notifications.mixins.WatchedModelMixin, models.Model),
+ ),
+ migrations.CreateModel(
+ name='RelatedUserStory',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order', models.IntegerField(default=10000, verbose_name='order')),
+ ('epic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epics.Epic')),
+ ('user_story', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='userstories.UserStory')),
+ ],
+ options={
+ 'ordering': ['user_story', 'order', 'id'],
+ 'verbose_name_plural': 'related user stories',
+ 'verbose_name': 'related user story',
+ },
+ ),
+ migrations.AddField(
+ model_name='epic',
+ name='user_stories',
+ field=models.ManyToManyField(related_name='epics', through='epics.RelatedUserStory', to='userstories.UserStory', verbose_name='user stories'),
+ ),
+ # Execute trigger after epic update
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_epic_update ON epics_epic;
+ CREATE TRIGGER update_project_tags_colors_on_epic_update
+ AFTER UPDATE ON epics_epic
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ # Execute trigger after epic insert
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_epic_insert ON epics_epic;
+ CREATE TRIGGER update_project_tags_colors_on_epic_insert
+ AFTER INSERT ON epics_epic
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ ]
diff --git a/taiga/projects/epics/migrations/0002_epic_color.py b/taiga/projects/epics/migrations/0002_epic_color.py
new file mode 100644
index 00000000..b9cd2ced
--- /dev/null
+++ b/taiga/projects/epics/migrations/0002_epic_color.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-27 09:37
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.colors
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('epics', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='epic',
+ name='color',
+ field=models.CharField(blank=True, default=taiga.base.utils.colors.generate_random_predefined_hex_color, max_length=32, verbose_name='color'),
+ ),
+ ]
diff --git a/taiga/projects/epics/migrations/0003_auto_20160901_1021.py b/taiga/projects/epics/migrations/0003_auto_20160901_1021.py
new file mode 100644
index 00000000..e23169f2
--- /dev/null
+++ b/taiga/projects/epics/migrations/0003_auto_20160901_1021.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-01 10:21
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('epics', '0002_epic_color'),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='relateduserstory',
+ unique_together=set([('user_story', 'epic')]),
+ ),
+ ]
diff --git a/taiga/projects/epics/migrations/0004_auto_20160928_0540.py b/taiga/projects/epics/migrations/0004_auto_20160928_0540.py
new file mode 100644
index 00000000..0e6a9fcb
--- /dev/null
+++ b/taiga/projects/epics/migrations/0004_auto_20160928_0540.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-28 05:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.time
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('epics', '0003_auto_20160901_1021'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='epic',
+ name='epics_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='epics order'),
+ ),
+ migrations.AlterField(
+ model_name='relateduserstory',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'),
+ ),
+ ]
diff --git a/taiga/projects/epics/migrations/__init__.py b/taiga/projects/epics/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py
new file mode 100644
index 00000000..da0e4a3e
--- /dev/null
+++ b/taiga/projects/epics/models.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.db import models
+from django.contrib.contenttypes.fields import GenericRelation
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+
+from taiga.base.utils.colors import generate_random_predefined_hex_color
+from taiga.base.utils.time import timestamp_ms
+from taiga.projects.tagging.models import TaggedMixin
+from taiga.projects.occ import OCCModelMixin
+from taiga.projects.notifications.mixins import WatchedModelMixin
+from taiga.projects.mixins.blocked import BlockedMixin
+
+
+class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
+ ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
+ verbose_name=_("ref"))
+ project = models.ForeignKey("projects.Project", null=False, blank=False,
+ related_name="epics", verbose_name=_("project"))
+ owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
+ related_name="owned_epics", verbose_name=_("owner"),
+ on_delete=models.SET_NULL)
+ status = models.ForeignKey("projects.EpicStatus", null=True, blank=True,
+ related_name="epics", verbose_name=_("status"),
+ on_delete=models.SET_NULL)
+ epics_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
+ verbose_name=_("epics order"))
+
+ created_date = models.DateTimeField(null=False, blank=False,
+ verbose_name=_("created date"),
+ default=timezone.now)
+ modified_date = models.DateTimeField(null=False, blank=False,
+ verbose_name=_("modified date"))
+
+ subject = models.TextField(null=False, blank=False,
+ verbose_name=_("subject"))
+ description = models.TextField(null=False, blank=True, verbose_name=_("description"))
+ color = models.CharField(max_length=32, null=False, blank=True,
+ default=generate_random_predefined_hex_color,
+ verbose_name=_("color"))
+ assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
+ default=None, related_name="epics_assigned_to_me",
+ verbose_name=_("assigned to"))
+ client_requirement = models.BooleanField(default=False, null=False, blank=True,
+ verbose_name=_("is client requirement"))
+ team_requirement = models.BooleanField(default=False, null=False, blank=True,
+ verbose_name=_("is team requirement"))
+
+ user_stories = models.ManyToManyField("userstories.UserStory", related_name="epics",
+ through='RelatedUserStory',
+ verbose_name=_("user stories"))
+
+ attachments = GenericRelation("attachments.Attachment")
+
+ _importing = None
+
+ class Meta:
+ verbose_name = "epic"
+ verbose_name_plural = "epics"
+ ordering = ["project", "epics_order", "ref"]
+
+ def __str__(self):
+ return "#{0} {1}".format(self.ref, self.subject)
+
+ def __repr__(self):
+ return "" % (self.id)
+
+ def save(self, *args, **kwargs):
+ if not self._importing or not self.modified_date:
+ self.modified_date = timezone.now()
+
+ if not self.status:
+ self.status = self.project.default_epic_status
+
+ super().save(*args, **kwargs)
+
+
+class RelatedUserStory(WatchedModelMixin, models.Model):
+ user_story = models.ForeignKey("userstories.UserStory", on_delete=models.CASCADE)
+ epic = models.ForeignKey("epics.Epic", on_delete=models.CASCADE)
+
+ order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
+ verbose_name=_("order"))
+
+ class Meta:
+ verbose_name = "related user story"
+ verbose_name_plural = "related user stories"
+ ordering = ["user_story", "order", "id"]
+ unique_together = (("user_story", "epic"), )
+
+ def __str__(self):
+ return "{0} - {1}".format(self.epic_id, self.user_story_id)
+
+ @property
+ def project(self):
+ return self.epic.project
+
+ @property
+ def project_id(self):
+ return self.epic.project_id
+
+ @property
+ def owner(self):
+ return self.epic.owner
+
+ @property
+ def owner_id(self):
+ return self.epic.owner_id
+
+ @property
+ def assigned_to_id(self):
+ return self.epic.assigned_to_id
diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py
new file mode 100644
index 00000000..fd473e18
--- /dev/null
+++ b/taiga/projects/epics/permissions.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated
+from taiga.base.api.permissions import IsSuperUser, HasProjectPerm, IsProjectAdmin
+
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
+
+
+class EpicPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_epics')
+ create_perms = HasProjectPerm('add_epic')
+ update_perms = CommentAndOrUpdatePerm('modify_epic', 'comment_epic')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_epic', 'comment_epic')
+ destroy_perms = HasProjectPerm('delete_epic')
+ list_perms = AllowAny()
+ filters_data_perms = AllowAny()
+ csv_perms = AllowAny()
+ bulk_create_perms = HasProjectPerm('add_epic')
+ upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics')
+ downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics')
+ watch_perms = IsAuthenticated() & HasProjectPerm('view_epics')
+ unwatch_perms = IsAuthenticated() & HasProjectPerm('view_epics')
+
+
+class EpicRelatedUserStoryPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_epics')
+ create_perms = HasProjectPerm('modify_epic')
+ update_perms = HasProjectPerm('modify_epic')
+ partial_update_perms = HasProjectPerm('modify_epic')
+ destroy_perms = HasProjectPerm('modify_epic')
+ list_perms = AllowAny()
+ bulk_create_perms = HasProjectPerm('modify_epic')
+
+
+class EpicVotersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_epics')
+ list_perms = HasProjectPerm('view_epics')
+
+
+class EpicWatchersPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
+ retrieve_perms = HasProjectPerm('view_epics')
+ list_perms = HasProjectPerm('view_epics')
diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py
new file mode 100644
index 00000000..339272de
--- /dev/null
+++ b/taiga/projects/epics/serializers.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
+from taiga.base.neighbors import NeighborsSerializerMixin
+
+from taiga.mdrender.service import render as mdrender
+from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
+from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer
+from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
+
+
+class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
+ TaggedInProjectResourceSerializer, serializers.LightSerializer):
+
+ id = Field()
+ ref = Field()
+ project = Field(attr="project_id")
+ created_date = Field()
+ modified_date = Field()
+ subject = Field()
+ color = Field()
+ epics_order = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ version = Field()
+ watchers = Field()
+ is_blocked = Field()
+ blocked_note = Field()
+ is_closed = MethodField()
+ user_stories_counts = MethodField()
+
+ def get_is_closed(self, obj):
+ return obj.status is not None and obj.status.is_closed
+
+ def get_user_stories_counts(self, obj):
+ assert hasattr(obj, "user_stories_counts"), "instance must have a user_stories_counts attribute"
+ return obj.user_stories_counts
+
+
+class EpicSerializer(EpicListSerializer):
+ comment = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
+
+ def get_comment(self, obj):
+ return ""
+
+ def get_blocked_note_html(self, obj):
+ return mdrender(obj.project, obj.blocked_note)
+
+ def get_description_html(self, obj):
+ return mdrender(obj.project, obj.description)
+
+
+class EpicNeighborsSerializer(NeighborsSerializerMixin, EpicSerializer):
+ pass
+
+
+class EpicRelatedUserStorySerializer(serializers.LightSerializer):
+ epic = Field(attr="epic_id")
+ user_story = Field(attr="user_story_id")
+ order = Field()
diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py
new file mode 100644
index 00000000..2921a35e
--- /dev/null
+++ b/taiga/projects/epics/services.py
@@ -0,0 +1,431 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import csv
+import io
+from collections import OrderedDict
+from operator import itemgetter
+from contextlib import closing
+
+from django.db import connection
+from django.utils.translation import ugettext as _
+
+from taiga.base.utils import db, text
+from taiga.projects.epics.apps import connect_epics_signals
+from taiga.projects.epics.apps import disconnect_epics_signals
+from taiga.projects.services import apply_order_updates
+from taiga.projects.userstories.apps import connect_userstories_signals
+from taiga.projects.userstories.apps import disconnect_userstories_signals
+from taiga.projects.userstories.services import get_userstories_from_bulk
+from taiga.events import events
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+
+from . import models
+
+
+#####################################################
+# Bulk actions
+#####################################################
+
+def get_epics_from_bulk(bulk_data, **additional_fields):
+ """Convert `bulk_data` into a list of epics.
+
+ :param bulk_data: List of epics in bulk format.
+ :param additional_fields: Additional fields when instantiating each epic.
+
+ :return: List of `Epic` instances.
+ """
+ return [models.Epic(subject=line, **additional_fields)
+ for line in text.split_in_lines(bulk_data)]
+
+
+def create_epics_in_bulk(bulk_data, callback=None, precall=None, **additional_fields):
+ """Create epics from `bulk_data`.
+
+ :param bulk_data: List of epics in bulk format.
+ :param callback: Callback to execute after each epic save.
+ :param additional_fields: Additional fields when instantiating each epic.
+
+ :return: List of created `Epic` instances.
+ """
+ epics = get_epics_from_bulk(bulk_data, **additional_fields)
+
+ disconnect_epics_signals()
+
+ try:
+ db.save_in_bulk(epics, callback, precall)
+ finally:
+ connect_epics_signals()
+
+ return epics
+
+
+def update_epics_order_in_bulk(bulk_data: list, field: str, project: object):
+ """
+ Update the order of some epics.
+ `bulk_data` should be a list of tuples with the following format:
+
+ [{'epic_id': , 'order': }, ...]
+ """
+ epics = project.epics.all()
+
+ epic_orders = {e.id: getattr(e, field) for e in epics}
+ new_epic_orders = {d["epic_id"]: d["order"] for d in bulk_data}
+ apply_order_updates(epic_orders, new_epic_orders)
+
+ epic_ids = epic_orders.keys()
+ events.emit_event_for_ids(ids=epic_ids,
+ content_type="epics.epic",
+ projectid=project.pk)
+
+ db.update_attr_in_bulk_for_ids(epic_orders, field, models.Epic)
+ return epic_orders
+
+
+def create_related_userstories_in_bulk(bulk_data, epic, **additional_fields):
+ """Create user stories from `bulk_data`.
+
+ :param epic: Element where all the user stories will be contained
+ :param bulk_data: List of user stories in bulk format.
+ :param additional_fields: Additional fields when instantiating each user story.
+
+ :return: List of created `Task` instances.
+ """
+ userstories = get_userstories_from_bulk(bulk_data, **additional_fields)
+ disconnect_userstories_signals()
+
+ try:
+ db.save_in_bulk(userstories)
+ related_userstories = []
+ for userstory in userstories:
+ related_userstories.append(
+ models.RelatedUserStory(
+ user_story=userstory,
+ epic=epic
+ )
+ )
+ db.save_in_bulk(related_userstories)
+ finally:
+ connect_userstories_signals()
+
+ return related_userstories
+
+
+def update_epic_related_userstories_order_in_bulk(bulk_data: list, epic: object):
+ """
+ Updates the order of the related userstories of an specific epic.
+ `bulk_data` should be a list of dicts with the following format:
+ `epic` is the epic with related stories.
+
+ [{'us_id': , 'order': }, ...]
+ """
+ related_user_stories = epic.relateduserstory_set.all()
+ # select_related
+ rus_orders = {rus.id: rus.order for rus in related_user_stories}
+
+ rus_conversion = {rus.user_story_id: rus.id for rus in related_user_stories}
+ new_rus_orders = {rus_conversion[e["us_id"]]: e["order"] for e in bulk_data
+ if e["us_id"] in rus_conversion}
+
+ apply_order_updates(rus_orders, new_rus_orders)
+
+ if rus_orders:
+ related_user_story_ids = rus_orders.keys()
+ events.emit_event_for_ids(ids=related_user_story_ids,
+ content_type="epics.relateduserstory",
+ projectid=epic.project_id)
+
+ db.update_attr_in_bulk_for_ids(rus_orders, "order", models.RelatedUserStory)
+
+ return rus_orders
+
+
+#####################################################
+# CSV
+#####################################################
+
+def epics_to_csv(project, queryset):
+ csv_data = io.StringIO()
+ fieldnames = ["ref", "subject", "description", "owner", "owner_full_name", "assigned_to",
+ "assigned_to_full_name", "status", "epics_order", "client_requirement",
+ "team_requirement", "attachments", "tags", "watchers", "voters",
+ "created_date", "modified_date", "related_user_stories"]
+
+ custom_attrs = project.epiccustomattributes.all()
+ for custom_attr in custom_attrs:
+ fieldnames.append(custom_attr.name)
+
+ queryset = queryset.prefetch_related("attachments",
+ "custom_attributes_values",
+ "user_stories__project")
+ queryset = queryset.select_related("owner",
+ "assigned_to",
+ "status",
+ "project")
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+
+ writer = csv.DictWriter(csv_data, fieldnames=fieldnames)
+ writer.writeheader()
+ for epic in queryset:
+ epic_data = {
+ "ref": epic.ref,
+ "subject": epic.subject,
+ "description": epic.description,
+ "owner": epic.owner.username if epic.owner else None,
+ "owner_full_name": epic.owner.get_full_name() if epic.owner else None,
+ "assigned_to": epic.assigned_to.username if epic.assigned_to else None,
+ "assigned_to_full_name": epic.assigned_to.get_full_name() if epic.assigned_to else None,
+ "status": epic.status.name if epic.status else None,
+ "epics_order": epic.epics_order,
+ "client_requirement": epic.client_requirement,
+ "team_requirement": epic.team_requirement,
+ "attachments": epic.attachments.count(),
+ "tags": ",".join(epic.tags or []),
+ "watchers": epic.watchers,
+ "voters": epic.total_voters,
+ "created_date": epic.created_date,
+ "modified_date": epic.modified_date,
+ "related_user_stories": ",".join([
+ "{}#{}".format(us.project.slug, us.ref) for us in epic.user_stories.all()
+ ]),
+ }
+
+ for custom_attr in custom_attrs:
+ value = epic.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
+ epic_data[custom_attr.name] = value
+
+ writer.writerow(epic_data)
+
+ return csv_data
+
+
+#####################################################
+# Api filter data
+#####################################################
+
+def _get_epics_statuses(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ SELECT "projects_epicstatus"."id",
+ "projects_epicstatus"."name",
+ "projects_epicstatus"."color",
+ "projects_epicstatus"."order",
+ (SELECT count(*)
+ FROM "epics_epic"
+ INNER JOIN "projects_project" ON
+ ("epics_epic"."project_id" = "projects_project"."id")
+ WHERE {where} AND "epics_epic"."status_id" = "projects_epicstatus"."id")
+ FROM "projects_epicstatus"
+ WHERE "projects_epicstatus"."project_id" = %s
+ ORDER BY "projects_epicstatus"."order";
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for id, name, color, order, count in rows:
+ result.append({
+ "id": id,
+ "name": _(name),
+ "color": color,
+ "order": order,
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("order"))
+
+
+def _get_epics_assigned_to(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ WITH counters AS (
+ SELECT assigned_to_id, count(assigned_to_id) count
+ FROM "epics_epic"
+ INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id")
+ WHERE {where} AND "epics_epic"."assigned_to_id" IS NOT NULL
+ GROUP BY assigned_to_id
+ )
+
+ SELECT "projects_membership"."user_id" user_id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
+ LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s
+ AND "projects_membership"."user_id" IS NOT NULL
+
+ -- unassigned epics
+ UNION
+
+ SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
+ FROM "epics_epic"
+ INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id")
+ WHERE {where} AND "epics_epic"."assigned_to_id" IS NULL
+ GROUP BY assigned_to_id
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id] + where_params)
+ rows = cursor.fetchall()
+
+ result = []
+ none_valued_added = False
+ for id, full_name, username, count in rows:
+ result.append({
+ "id": id,
+ "full_name": full_name or username or "",
+ "count": count,
+ })
+
+ if id is None:
+ none_valued_added = True
+
+ # If there was no epic with null assigned_to we manually add it
+ if not none_valued_added:
+ result.append({
+ "id": None,
+ "full_name": "",
+ "count": 0,
+ })
+
+ return sorted(result, key=itemgetter("full_name"))
+
+
+def _get_epics_owners(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ WITH counters AS (
+ SELECT "epics_epic"."owner_id" owner_id,
+ count(coalesce("epics_epic"."owner_id", -1)) count
+ FROM "epics_epic"
+ INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY "epics_epic"."owner_id"
+ )
+
+ SELECT "projects_membership"."user_id" id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
+ LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s
+ AND "projects_membership"."user_id" IS NOT NULL
+
+ -- System users
+ UNION
+
+ SELECT "users_user"."id" user_id,
+ "users_user"."full_name" full_name,
+ "users_user"."username" username,
+ COALESCE("counters".count, 0) count
+ FROM users_user
+ LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
+ WHERE ("users_user"."is_system" IS TRUE)
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for id, full_name, username, count in rows:
+ if count > 0:
+ result.append({
+ "id": id,
+ "full_name": full_name or username or "",
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("full_name"))
+
+
+def _get_epics_tags(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ WITH epics_tags AS (
+ SELECT tag,
+ COUNT(tag) counter FROM (
+ SELECT UNNEST(epics_epic.tags) tag
+ FROM epics_epic
+ INNER JOIN projects_project
+ ON (epics_epic.project_id = projects_project.id)
+ WHERE {where}) tags
+ GROUP BY tag),
+ project_tags AS (
+ SELECT reduce_dim(tags_colors) tag_color
+ FROM projects_project
+ WHERE id=%s)
+
+ SELECT tag_color[1] tag,
+ tag_color[2] color,
+ COALESCE(epics_tags.counter, 0) counter
+ FROM project_tags
+ LEFT JOIN epics_tags ON project_tags.tag_color[1] = epics_tags.tag
+ ORDER BY tag
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for name, color, count in rows:
+ result.append({
+ "name": name,
+ "color": color,
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("name"))
+
+
+def get_epics_filters_data(project, querysets):
+ """
+ Given a project and an epics queryset, return a simple data structure
+ of all possible filters for the epics in the queryset.
+ """
+ data = OrderedDict([
+ ("statuses", _get_epics_statuses(project, querysets["statuses"])),
+ ("assigned_to", _get_epics_assigned_to(project, querysets["assigned_to"])),
+ ("owners", _get_epics_owners(project, querysets["owners"])),
+ ("tags", _get_epics_tags(project, querysets["tags"])),
+ ])
+
+ return data
diff --git a/taiga/projects/epics/utils.py b/taiga/projects/epics/utils.py
new file mode 100644
index 00000000..49e394d8
--- /dev/null
+++ b/taiga/projects/epics/utils.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.attachments.utils import attach_basic_attachments
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False):
+ if include_attachments:
+ queryset = attach_basic_attachments(queryset)
+ queryset = queryset.extra(select={"include_attachments": "True"})
+
+ queryset = attach_user_stories_counts_to_queryset(queryset)
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
+
+
+def attach_user_stories_counts_to_queryset(queryset, as_field="user_stories_counts"):
+ model = queryset.model
+ sql = """SELECT (SELECT row_to_json(t)
+ FROM (SELECT COALESCE(SUM(CASE WHEN is_closed IS FALSE THEN 1 ELSE 0 END), 0) AS "opened",
+ COALESCE(SUM(CASE WHEN is_closed IS TRUE THEN 1 ELSE 0 END), 0) AS "closed"
+ ) t
+ )
+ FROM epics_relateduserstory
+ INNER JOIN userstories_userstory ON epics_relateduserstory.user_story_id = userstories_userstory.id
+ WHERE epics_relateduserstory.epic_id = {tbl}.id"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py
new file mode 100644
index 00000000..7ed00481
--- /dev/null
+++ b/taiga/projects/epics/validators.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.utils.translation import ugettext as _
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import PgArrayField
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.userstories.validators import UserStoryExistsValidator
+from taiga.projects.validators import ProjectExistsValidator
+from . import models
+
+
+class EpicExistsValidator:
+ def validate_epic_id(self, attrs, source):
+ value = attrs[source]
+ if not models.Epic.objects.filter(pk=value).exists():
+ msg = _("There's no epic with that id")
+ raise ValidationError(msg)
+ return attrs
+
+
+class EpicValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+
+ class Meta:
+ model = models.Epic
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+
+class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator,
+ validators.Validator):
+ project_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ bulk_epics = serializers.CharField()
+
+
+class CreateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator,
+ validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_userstories = serializers.CharField()
+
+
+
+class EpicRelatedUserStoryValidator(validators.ModelValidator):
+ class Meta:
+ model = models.RelatedUserStory
+ read_only_fields = ('id',)
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index b3be1a0a..fe720f97 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -45,7 +45,7 @@ class DiscoverModeFilterBackend(FilterBackend):
if request.QUERY_PARAMS.get("is_featured", None) == 'true':
qs = qs.order_by("?")
- return super().filter_queryset(request, qs.distinct(), view)
+ return super().filter_queryset(request, qs, view)
class CanViewProjectObjFilterBackend(FilterBackend):
@@ -86,7 +86,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
# external users / anonymous
qs = qs.filter(anon_permissions__contains=["view_project"])
- return super().filter_queryset(request, qs.distinct(), view)
+ return super().filter_queryset(request, qs, view)
class QFilterBackend(FilterBackend):
@@ -97,12 +97,12 @@ class QFilterBackend(FilterBackend):
tsquery = "to_tsquery('english_nostop', %s)"
tsquery_params = [to_tsquery(q)]
tsvector = """
- setweight(to_tsvector('english_nostop',
- coalesce(projects_project.name, '')), 'A') ||
- setweight(to_tsvector('english_nostop',
- coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') ||
- setweight(to_tsvector('english_nostop',
- coalesce(projects_project.description, '')), 'C')
+ setweight(to_tsvector('english_nostop',
+ coalesce(projects_project.name, '')), 'A') ||
+ setweight(to_tsvector('english_nostop',
+ coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') ||
+ setweight(to_tsvector('english_nostop',
+ coalesce(projects_project.description, '')), 'C')
"""
select = {
@@ -111,7 +111,7 @@ class QFilterBackend(FilterBackend):
}
select_params = tsquery_params
where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery,
- tsvector=tsvector),]
+ tsvector=tsvector), ]
params = tsquery_params
order_by = ["-rank", ]
@@ -121,3 +121,34 @@ class QFilterBackend(FilterBackend):
params=params,
order_by=order_by)
return queryset
+
+
+class UserOrderFilterBackend(FilterBackend):
+ def filter_queryset(self, request, queryset, view):
+ if request.user.is_anonymous():
+ return queryset
+
+ raw_fieldname = request.QUERY_PARAMS.get(self.order_by_query_param, None)
+ if not raw_fieldname:
+ return queryset
+
+ if raw_fieldname.startswith("-"):
+ field_name = raw_fieldname[1:]
+ else:
+ field_name = raw_fieldname
+
+ if field_name != "user_order":
+ return queryset
+
+ model = queryset.model
+ sql = """SELECT projects_membership.user_order
+ FROM projects_membership
+ WHERE
+ projects_membership.project_id = {tbl}.id AND
+ projects_membership.user_id = {user_id}
+ """
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id)
+ queryset = queryset.extra(select={"user_order": sql})
+ queryset = queryset.order_by(raw_fieldname)
+ return queryset
diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json
index 46d369a5..6137792f 100644
--- a/taiga/projects/fixtures/initial_project_templates.json
+++ b/taiga/projects/fixtures/initial_project_templates.json
@@ -1,56 +1,62 @@
[
{
"model": "projects.projecttemplate",
+ "pk": 1,
"fields": {
- "is_issues_activated": true,
- "task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff9900\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#ffcc00\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#999999\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
- "is_backlog_activated": true,
- "modified_date": "2014-07-25T10:02:46.479Z",
- "us_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff8a84\", \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\"}, {\"color\": \"#ff9900\", \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#fcc000\", \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\"}, {\"color\": \"#5c3566\", \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\"}]",
- "is_wiki_activated": true,
- "roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
- "points": "[{\"value\": null, \"order\": 1, \"name\": \"?\"}, {\"value\": 0.0, \"order\": 2, \"name\": \"0\"}, {\"value\": 0.5, \"order\": 3, \"name\": \"1/2\"}, {\"value\": 1.0, \"order\": 4, \"name\": \"1\"}, {\"value\": 2.0, \"order\": 5, \"name\": \"2\"}, {\"value\": 3.0, \"order\": 6, \"name\": \"3\"}, {\"value\": 5.0, \"order\": 7, \"name\": \"5\"}, {\"value\": 8.0, \"order\": 8, \"name\": \"8\"}, {\"value\": 10.0, \"order\": 9, \"name\": \"10\"}, {\"value\": 13.0, \"order\": 10, \"name\": \"13\"}, {\"value\": 20.0, \"order\": 11, \"name\": \"20\"}, {\"value\": 40.0, \"order\": 12, \"name\": \"40\"}]",
- "severities": "[{\"color\": \"#666666\", \"order\": 1, \"name\": \"Wishlist\"}, {\"color\": \"#669933\", \"order\": 2, \"name\": \"Minor\"}, {\"color\": \"#0000FF\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#FFA500\", \"order\": 4, \"name\": \"Important\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"Critical\"}]",
- "is_kanban_activated": false,
- "priorities": "[{\"color\": \"#666666\", \"order\": 1, \"name\": \"Low\"}, {\"color\": \"#669933\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"High\"}]",
- "created_date": "2014-04-22T14:48:43.596Z",
- "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}",
+ "name": "Scrum",
"slug": "scrum",
- "videoconferences_extra_data": "",
- "issue_statuses": "[{\"color\": \"#8C2318\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#5E8C6A\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#88A65E\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#BFB35A\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#89BAB4\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#CC0000\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#666666\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]",
- "default_owner_role": "product-owner",
- "issue_types": "[{\"color\": \"#89BAB4\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#ba89a8\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#89a8ba\", \"order\": 3, \"name\": \"Enhancement\"}]",
- "videoconferences": null,
"description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers",
- "name": "Scrum"
- },
- "pk": 1
+ "order": 1,
+ "created_date": "2014-04-22T14:48:43.596Z",
+ "modified_date": "2016-08-24T16:26:40.845Z",
+ "default_owner_role": "product-owner",
+ "is_epics_activated": false,
+ "is_backlog_activated": true,
+ "is_kanban_activated": false,
+ "is_wiki_activated": true,
+ "is_issues_activated": true,
+ "videoconferences": null,
+ "videoconferences_extra_data": "",
+ "default_options": "{\"epic_status\": \"New\", \"issue_status\": \"New\", \"task_status\": \"New\", \"points\": \"?\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"priority\": \"Normal\", \"us_status\": \"New\"}",
+ "epic_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"order\": 2}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"order\": 3}, {\"is_closed\": false, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"order\": 4}, {\"is_closed\": true, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"order\": 5}]",
+ "us_statuses": "[{\"is_archived\": false, \"name\": \"New\", \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready\", \"slug\": \"ready\", \"order\": 2, \"color\": \"#ff8a84\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"In progress\", \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#ff9900\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#fcc000\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Done\", \"slug\": \"done\", \"order\": 5, \"color\": \"#669900\", \"wip_limit\": null, \"is_closed\": true}, {\"is_archived\": true, \"name\": \"Archived\", \"slug\": \"archived\", \"order\": 6, \"color\": \"#5c3566\", \"wip_limit\": null, \"is_closed\": true}]",
+ "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]",
+ "task_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#ffcc00\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#669900\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#999999\", \"slug\": \"needs-info\", \"order\": 5}]",
+ "issue_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#8C2318\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#5E8C6A\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#88A65E\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#BFB35A\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#89BAB4\", \"slug\": \"needs-info\", \"order\": 5}, {\"is_closed\": true, \"name\": \"Rejected\", \"color\": \"#CC0000\", \"slug\": \"rejected\", \"order\": 6}, {\"is_closed\": false, \"name\": \"Postponed\", \"color\": \"#666666\", \"slug\": \"posponed\", \"order\": 7}]",
+ "issue_types": "[{\"name\": \"Bug\", \"color\": \"#89BAB4\", \"order\": 1}, {\"name\": \"Question\", \"color\": \"#ba89a8\", \"order\": 2}, {\"name\": \"Enhancement\", \"color\": \"#89a8ba\", \"order\": 3}]",
+ "priorities": "[{\"name\": \"Low\", \"color\": \"#666666\", \"order\": 1}, {\"name\": \"Normal\", \"color\": \"#669933\", \"order\": 3}, {\"name\": \"High\", \"color\": \"#CC0000\", \"order\": 5}]",
+ "severities": "[{\"name\": \"Wishlist\", \"color\": \"#666666\", \"order\": 1}, {\"name\": \"Minor\", \"color\": \"#669933\", \"order\": 2}, {\"name\": \"Normal\", \"color\": \"#0000FF\", \"order\": 3}, {\"name\": \"Important\", \"color\": \"#FFA500\", \"order\": 4}, {\"name\": \"Critical\", \"color\": \"#CC0000\", \"order\": 5}]",
+ "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 60}]"
+ }
},
{
"model": "projects.projecttemplate",
+ "pk": 2,
"fields": {
- "is_issues_activated": false,
- "task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]",
- "is_backlog_activated": false,
- "modified_date": "2014-07-25T13:11:42.754Z",
- "us_statuses": "[{\"wip_limit\": null, \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"name\": \"New\", \"slug\": \"new\"}, {\"wip_limit\": null, \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"color\": \"#f57900\", \"name\": \"Ready\", \"slug\": \"ready\"}, {\"wip_limit\": null, \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"color\": \"#729fcf\", \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"wip_limit\": null, \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"color\": \"#4e9a06\", \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"wip_limit\": null, \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"color\": \"#cc0000\", \"name\": \"Done\", \"slug\": \"done\"}, {\"wip_limit\": null, \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"name\": \"Archived\", \"slug\": \"archived\"}]",
- "is_wiki_activated": false,
- "roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]",
- "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]",
- "severities": "[{\"color\": \"#999999\", \"order\": 1, \"name\": \"Wishlist\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Minor\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#f57900\", \"order\": 4, \"name\": \"Important\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"Critical\"}]",
- "is_kanban_activated": true,
- "priorities": "[{\"color\": \"#999999\", \"order\": 1, \"name\": \"Low\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"High\"}]",
- "created_date": "2014-04-22T14:50:19.738Z",
- "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}",
+ "name": "Kanban",
"slug": "kanban",
- "videoconferences_extra_data": "",
- "issue_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#d3d7cf\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#75507b\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]",
- "default_owner_role": "product-owner",
- "issue_types": "[{\"color\": \"#cc0000\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Enhancement\"}]",
- "videoconferences": null,
"description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.",
- "name": "Kanban"
- },
- "pk": 2
+ "order": 2,
+ "created_date": "2014-04-22T14:50:19.738Z",
+ "modified_date": "2016-08-24T16:26:45.365Z",
+ "default_owner_role": "product-owner",
+ "is_epics_activated": false,
+ "is_backlog_activated": false,
+ "is_kanban_activated": true,
+ "is_wiki_activated": false,
+ "is_issues_activated": false,
+ "videoconferences": null,
+ "videoconferences_extra_data": "",
+ "default_options": "{\"epic_status\": \"New\", \"issue_status\": \"New\", \"task_status\": \"New\", \"points\": \"?\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"priority\": \"Normal\", \"us_status\": \"New\"}",
+ "epic_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"order\": 2}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"order\": 3}, {\"is_closed\": false, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"order\": 4}, {\"is_closed\": true, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"order\": 5}]",
+ "us_statuses": "[{\"is_archived\": false, \"name\": \"New\", \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready\", \"slug\": \"ready\", \"order\": 2, \"color\": \"#f57900\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"In progress\", \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#729fcf\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#4e9a06\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Done\", \"slug\": \"done\", \"order\": 5, \"color\": \"#cc0000\", \"wip_limit\": null, \"is_closed\": true}, {\"is_archived\": true, \"name\": \"Archived\", \"slug\": \"archived\", \"order\": 6, \"color\": \"#5c3566\", \"wip_limit\": null, \"is_closed\": true}]",
+ "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]",
+ "task_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"order\": 5}]",
+ "issue_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"order\": 5}, {\"is_closed\": true, \"name\": \"Rejected\", \"color\": \"#d3d7cf\", \"slug\": \"rejected\", \"order\": 6}, {\"is_closed\": false, \"name\": \"Postponed\", \"color\": \"#75507b\", \"slug\": \"posponed\", \"order\": 7}]",
+ "issue_types": "[{\"name\": \"Bug\", \"color\": \"#cc0000\", \"order\": 1}, {\"name\": \"Question\", \"color\": \"#729fcf\", \"order\": 2}, {\"name\": \"Enhancement\", \"color\": \"#4e9a06\", \"order\": 3}]",
+ "priorities": "[{\"name\": \"Low\", \"color\": \"#999999\", \"order\": 1}, {\"name\": \"Normal\", \"color\": \"#4e9a06\", \"order\": 3}, {\"name\": \"High\", \"color\": \"#CC0000\", \"order\": 5}]",
+ "severities": "[{\"name\": \"Wishlist\", \"color\": \"#999999\", \"order\": 1}, {\"name\": \"Minor\", \"color\": \"#729fcf\", \"order\": 2}, {\"name\": \"Normal\", \"color\": \"#4e9a06\", \"order\": 3}, {\"name\": \"Important\", \"color\": \"#f57900\", \"order\": 4}, {\"name\": \"Critical\", \"color\": \"#CC0000\", \"order\": 5}]",
+ "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 60}]"
+ }
}
]
diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py
index 281d8012..17c2fa83 100644
--- a/taiga/projects/history/api.py
+++ b/taiga/projects/history/api.py
@@ -23,7 +23,7 @@ from django.utils import timezone
from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import ReadOnlyListViewSet
-from taiga.base.api.utils import get_object_or_404
+from taiga.mdrender.service import render as mdrender
from . import permissions
from . import serializers
@@ -37,7 +37,7 @@ class HistoryViewSet(ReadOnlyListViewSet):
def get_content_type(self):
app_name, model = self.content_type.split(".", 1)
- return get_object_or_404(ContentType, app_label=app_name, model=model)
+ return ContentType.objects.get_by_natural_key(app_name, model)
def get_queryset(self):
ct = self.get_content_type()
@@ -57,42 +57,102 @@ class HistoryViewSet(ReadOnlyListViewSet):
return response.Ok(serializer.data)
+ @detail_route(methods=['get'])
+ def comment_versions(self, request, pk):
+ obj = self.get_object()
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
+ if history_entry is None:
+ return response.NotFound()
+
+ self.check_permissions(request, 'comment_versions', history_entry)
+
+ if history_entry is None:
+ return response.NotFound()
+
+ history_entry.attach_user_info_to_comment_versions()
+ return response.Ok(history_entry.comment_versions)
+
+ @detail_route(methods=['post'])
+ def edit_comment(self, request, pk):
+ obj = self.get_object()
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
+ if history_entry is None:
+ return response.NotFound()
+
+ obj = services.get_instance_from_key(history_entry.key)
+ comment = request.DATA.get("comment", None)
+
+ self.check_permissions(request, 'edit_comment', history_entry)
+
+ if history_entry is None:
+ return response.NotFound()
+
+ if comment is None:
+ return response.BadRequest({"error": _("comment is required")})
+
+ if history_entry.delete_comment_date or history_entry.delete_comment_user:
+ return response.BadRequest({"error": _("deleted comments can't be edited")})
+
+ # comment_versions can be None if there are no historic versions of the comment
+ comment_versions = history_entry.comment_versions or []
+ comment_versions.append({
+ "date": history_entry.created_at,
+ "comment": history_entry.comment,
+ "comment_html": history_entry.comment_html,
+ "user": {
+ "id": request.user.pk,
+ }
+ })
+
+ history_entry.edit_comment_date = timezone.now()
+ history_entry.comment = comment
+ history_entry.comment_html = mdrender(obj.project, comment)
+ history_entry.comment_versions = comment_versions
+ history_entry.save()
+ return response.Ok()
+
@detail_route(methods=['post'])
def delete_comment(self, request, pk):
obj = self.get_object()
- comment_id = request.QUERY_PARAMS.get('id', None)
- comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first()
-
- self.check_permissions(request, 'delete_comment', comment)
-
- if comment is None:
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
+ if history_entry is None:
return response.NotFound()
- if comment.delete_comment_date or comment.delete_comment_user:
+ self.check_permissions(request, 'delete_comment', history_entry)
+
+ if history_entry is None:
+ return response.NotFound()
+
+ if history_entry.delete_comment_date or history_entry.delete_comment_user:
return response.BadRequest({"error": _("Comment already deleted")})
- comment.delete_comment_date = timezone.now()
- comment.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()}
- comment.save()
+ history_entry.delete_comment_date = timezone.now()
+ history_entry.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()}
+ history_entry.save()
return response.Ok()
@detail_route(methods=['post'])
def undelete_comment(self, request, pk):
obj = self.get_object()
- comment_id = request.QUERY_PARAMS.get('id', None)
- comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first()
-
- self.check_permissions(request, 'undelete_comment', comment)
-
- if comment is None:
+ history_entry_id = request.QUERY_PARAMS.get('id', None)
+ history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first()
+ if history_entry is None:
return response.NotFound()
- if not comment.delete_comment_date and not comment.delete_comment_user:
+ self.check_permissions(request, 'undelete_comment', history_entry)
+
+ if history_entry is None:
+ return response.NotFound()
+
+ if not history_entry.delete_comment_date and not history_entry.delete_comment_user:
return response.BadRequest({"error": _("Comment not deleted")})
- comment.delete_comment_date = None
- comment.delete_comment_user = None
- comment.save()
+ history_entry.delete_comment_date = None
+ history_entry.delete_comment_user = None
+ history_entry.save()
return response.Ok()
# Just for restframework! Because it raises
@@ -108,6 +168,11 @@ class HistoryViewSet(ReadOnlyListViewSet):
return self.response_for_queryset(qs)
+class EpicHistory(HistoryViewSet):
+ content_type = "epics.epic"
+ permission_classes = (permissions.EpicHistoryPermission,)
+
+
class UserStoryHistory(HistoryViewSet):
content_type = "userstories.userstory"
permission_classes = (permissions.UserStoryHistoryPermission,)
diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py
index 9b2dcadc..fd452257 100644
--- a/taiga/projects/history/freeze_impl.py
+++ b/taiga/projects/history/freeze_impl.py
@@ -106,6 +106,20 @@ def milestone_values(diff):
return values
+def epic_values(diff):
+ values = _common_users_values(diff)
+
+ if "status" in diff:
+ values["status"] = _get_us_status_values(diff["status"])
+
+ return values
+
+
+def epic_related_userstory_values(diff):
+ values = _common_users_values(diff)
+ return values
+
+
def userstory_values(diff):
values = _common_users_values(diff)
@@ -190,6 +204,18 @@ def extract_attachments(obj) -> list:
"order": attach.order}
+@as_tuple
+def extract_epic_custom_attributes(obj) -> list:
+ with suppress(ObjectDoesNotExist):
+ custom_attributes_values = obj.custom_attributes_values.attributes_values
+ for attr in obj.project.epiccustomattributes.all():
+ with suppress(KeyError):
+ value = custom_attributes_values[str(attr.id)]
+ yield {"id": attr.id,
+ "name": attr.name,
+ "value": value}
+
+
@as_tuple
def extract_user_story_custom_attributes(obj) -> list:
with suppress(ObjectDoesNotExist):
@@ -235,6 +261,7 @@ def project_freezer(project) -> dict:
"total_milestones",
"total_story_points",
"tags",
+ "is_epics_activated",
"is_backlog_activated",
"is_kanban_activated",
"is_wiki_activated",
@@ -256,6 +283,40 @@ def milestone_freezer(milestone) -> dict:
return snapshot
+def epic_freezer(epic) -> dict:
+ snapshot = {
+ "ref": epic.ref,
+ "color": epic.color,
+ "owner": epic.owner_id,
+ "status": epic.status.id if epic.status else None,
+ "epics_order": epic.epics_order,
+ "subject": epic.subject,
+ "description": epic.description,
+ "description_html": mdrender(epic.project, epic.description),
+ "assigned_to": epic.assigned_to_id,
+ "client_requirement": epic.client_requirement,
+ "team_requirement": epic.team_requirement,
+ "attachments": extract_attachments(epic),
+ "tags": epic.tags,
+ "is_blocked": epic.is_blocked,
+ "blocked_note": epic.blocked_note,
+ "blocked_note_html": mdrender(epic.project, epic.blocked_note),
+ "custom_attributes": extract_epic_custom_attributes(epic)
+ }
+
+ return snapshot
+
+
+def epic_related_userstory_freezer(related_us) -> dict:
+ snapshot = {
+ "user_story": related_us.user_story.id,
+ "epic": related_us.epic.id,
+ "order": related_us.order
+ }
+
+ return snapshot
+
+
def userstory_freezer(us) -> dict:
rp_cls = apps.get_model("userstories", "RolePoints")
rpqsd = rp_cls.objects.filter(user_story=us)
diff --git a/taiga/projects/history/migrations/0009_auto_20160512_1110.py b/taiga/projects/history/migrations/0009_auto_20160512_1110.py
new file mode 100644
index 00000000..0cf39023
--- /dev/null
+++ b/taiga/projects/history/migrations/0009_auto_20160512_1110.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-12 11:10
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django_pgjson.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('history', '0008_auto_20150508_1028'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='historyentry',
+ name='comment_versions',
+ field=django_pgjson.fields.JsonField(blank=True, default=None, null=True),
+ ),
+ migrations.AddField(
+ model_name='historyentry',
+ name='edit_comment_date',
+ field=models.DateTimeField(blank=True, default=None, null=True),
+ ),
+ ]
diff --git a/taiga/projects/history/migrations/0010_historyentry_project.py b/taiga/projects/history/migrations/0010_historyentry_project.py
new file mode 100644
index 00000000..0949a9a8
--- /dev/null
+++ b/taiga/projects/history/migrations/0010_historyentry_project.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-24 12:19
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0048_auto_20160615_1508'),
+ ('history', '0009_auto_20160512_1110'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='historyentry',
+ name='project',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='projects.Project'),
+ ),
+ ]
diff --git a/taiga/projects/history/migrations/0011_auto_20160629_1036.py b/taiga/projects/history/migrations/0011_auto_20160629_1036.py
new file mode 100644
index 00000000..698f6a21
--- /dev/null
+++ b/taiga/projects/history/migrations/0011_auto_20160629_1036.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-29 10:36
+from __future__ import unicode_literals
+
+from django.db import migrations, connection
+from taiga.projects.history.services import get_instance_from_key
+
+
+GENERATE_CORRECT_HISTORY_ENTRIES_TABLE = """
+ -- Creating a table containing all the existing object keys and the project ids
+ DROP TABLE IF EXISTS project_keys;
+ CREATE TABLE project_keys (
+ key VARCHAR,
+ project_id INTEGER
+ );
+
+ DROP INDEX IF EXISTS project_keys_index;
+ CREATE INDEX project_keys_index
+ ON project_keys
+ USING btree
+ (key);
+
+ INSERT INTO project_keys
+ SELECT 'milestones.milestone:' || id, project_id
+ FROM milestones_milestone;
+
+ INSERT INTO project_keys
+ SELECT 'userstories.userstory:' || id, project_id
+ FROM userstories_userstory;
+
+ INSERT INTO project_keys
+ SELECT 'tasks.task:' || id, project_id
+ FROM tasks_task;
+
+ INSERT INTO project_keys
+ SELECT 'issues.issue:' || id, project_id
+ FROM issues_issue;
+
+ INSERT INTO project_keys
+ SELECT 'wiki.wikipage:' || id, project_id
+ FROM wiki_wikipage;
+
+ INSERT INTO project_keys
+ SELECT 'projects.project:' || id, id
+ FROM projects_project;
+
+ -- Create a table where we will insert all the history_historyentry content with its correct project_id
+ -- Elements without project_id won't be inserted
+ DROP TABLE IF EXISTS history_historyentry_correct;
+ CREATE TABLE history_historyentry_correct AS
+ SELECT
+ history_historyentry.id ,
+ history_historyentry.user,
+ history_historyentry.created_at,
+ history_historyentry.type,
+ history_historyentry.is_snapshot,
+ history_historyentry.key,
+ history_historyentry.diff,
+ history_historyentry.snapshot,
+ history_historyentry.values,
+ history_historyentry.comment,
+ history_historyentry.comment_html,
+ history_historyentry.delete_comment_date,
+ history_historyentry.delete_comment_user,
+ history_historyentry.is_hidden,
+ history_historyentry.comment_versions,
+ history_historyentry.edit_comment_date,
+ project_keys.project_id
+ FROM history_historyentry
+ INNER JOIN project_keys
+ ON project_keys.key = history_historyentry.key;
+
+ -- Delete aux table
+ DROP TABLE IF EXISTS project_keys;
+ """
+
+def get_constraints_def_sql(table_name):
+ cursor = connection.cursor()
+ query = """
+ SELECT 'ALTER TABLE "'||nspname||'"."'||relname||'" ADD CONSTRAINT "'||conname||'" '||
+ pg_get_constraintdef(pg_constraint.oid)||';'
+ FROM pg_constraint
+ INNER JOIN pg_class ON conrelid=pg_class.oid
+ INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace
+ WHERE relname='{}'
+ ORDER BY CASE WHEN contype='f' THEN 0 ELSE 1 END DESC,contype DESC,nspname DESC,relname DESC,conname DESC;
+ """.format(table_name)
+ cursor.execute(query)
+ return [row[0] for row in cursor.fetchall()]
+
+
+def get_indexes_def_sql(table_name):
+ cursor = connection.cursor()
+ query = """
+ SELECT pg_get_indexdef(idx.oid)||';'
+ FROM pg_index ind
+ JOIN pg_class idx ON idx.oid = ind.indexrelid
+ JOIN pg_class tbl ON tbl.oid = ind.indrelid
+ LEFT JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
+ WHERE
+ tbl.relname = '{}' AND
+ indisprimary=FALSE;
+ """.format(table_name)
+ cursor.execute(query)
+ return [row[0] for row in cursor.fetchall()]
+
+
+def drop_constraints(table_name):
+ # This query returns all the ALTER sentences needed to drop the constraints
+ cursor = connection.cursor()
+ alter_sentences_query = """
+ SELECT 'ALTER TABLE "'||nspname||'"."'||relname||'" DROP CONSTRAINT "'||conname||'" '||';'
+ FROM pg_constraint
+ INNER JOIN pg_class ON conrelid=pg_class.oid
+ INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace
+ WHERE relname='{}'
+ ORDER BY CASE WHEN contype='f' THEN 0 ELSE 1 END DESC,contype DESC,nspname DESC,relname DESC,conname DESC;
+ """.format(table_name)
+ cursor.execute(alter_sentences_query)
+ alter_sentences = [row[0] for row in cursor.fetchall()]
+
+ #Now we execute those sentences
+ for alter_sentence in alter_sentences:
+ cursor.execute(alter_sentence)
+
+
+def toggle_history_entries_tables(apps, schema_editor):
+ history_entry_sql_def_contraints = get_constraints_def_sql("history_historyentry")
+ history_entry_sql_def_indexes = get_indexes_def_sql("history_historyentry")
+ history_change_notifications_sql_def_contraints = get_constraints_def_sql("notifications_historychangenotification_history_entries")
+ drop_constraints("notifications_historychangenotification_history_entries")
+ cursor = connection.cursor()
+ cursor.execute("""
+ DELETE FROM notifications_historychangenotification_history_entries;
+ DROP TABLE history_historyentry;
+ ALTER TABLE "history_historyentry_correct" RENAME to "history_historyentry";
+ """)
+
+ for history_entry_sql_def_contraint in history_entry_sql_def_contraints:
+ cursor.execute(history_entry_sql_def_contraint)
+
+ for history_entry_sql_def_index in history_entry_sql_def_indexes:
+ cursor.execute(history_entry_sql_def_index)
+
+ # Restoring the dropped constraints and indexes
+ for history_change_notifications_sql_def_contraint in history_change_notifications_sql_def_contraints:
+ cursor.execute(history_change_notifications_sql_def_contraint)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('history', '0010_historyentry_project'),
+ ('wiki', '0003_auto_20160615_0721'),
+ ('users', '0022_auto_20160629_1443')
+ ]
+
+ operations = [
+ migrations.RunSQL(GENERATE_CORRECT_HISTORY_ENTRIES_TABLE),
+ migrations.RunPython(toggle_history_entries_tables)
+ ]
diff --git a/taiga/projects/history/migrations/0012_auto_20160629_1036.py b/taiga/projects/history/migrations/0012_auto_20160629_1036.py
new file mode 100644
index 00000000..549d7076
--- /dev/null
+++ b/taiga/projects/history/migrations/0012_auto_20160629_1036.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-29 10:36
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('history', '0011_auto_20160629_1036'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='historyentry',
+ name='project',
+ field=models.ForeignKey(on_delete=models.deletion.CASCADE, to='projects.Project'),
+ ),
+ ]
diff --git a/taiga/projects/history/mixins.py b/taiga/projects/history/mixins.py
index 0a70366d..14a0f44d 100644
--- a/taiga/projects/history/mixins.py
+++ b/taiga/projects/history/mixins.py
@@ -62,7 +62,7 @@ class HistoryResourceMixin(object):
obj = self.get_object()
sobj = self.get_object_for_snapshot(obj)
- if sobj != obj and delete:
+ if sobj != obj:
delete = False
notifications_services.analize_object_for_watchers(obj, comment, user)
diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py
index 1aedd223..4697875c 100644
--- a/taiga/projects/history/models.py
+++ b/taiga/projects/history/models.py
@@ -33,7 +33,8 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
# This keys has been removed from freeze_impl so we can have objects where the
# previous diff has value for the attribute and we want to prevent their propagation
-IGNORE_DIFF_FIELDS = [ "watchers", "description_diff", "content_diff", "blocked_note_diff"]
+IGNORE_DIFF_FIELDS = ["watchers", "description_diff", "content_diff", "blocked_note_diff"]
+
def _generate_uuid():
return str(uuid.uuid1())
@@ -49,6 +50,7 @@ class HistoryEntry(models.Model):
"""
id = models.CharField(primary_key=True, max_length=255, unique=True,
editable=False, default=_generate_uuid)
+ project = models.ForeignKey("projects.Project")
user = JsonField(null=True, blank=True, default=None)
created_at = models.DateTimeField(default=timezone.now)
@@ -71,6 +73,10 @@ class HistoryEntry(models.Model):
delete_comment_date = models.DateTimeField(null=True, blank=True, default=None)
delete_comment_user = JsonField(null=True, blank=True, default=None)
+ # Historic version of comments
+ comment_versions = JsonField(null=True, blank=True, default=None)
+ edit_comment_date = models.DateTimeField(null=True, blank=True, default=None)
+
# Flag for mark some history entries as
# hidden. Hidden history entries are important
# for save but not important to preview.
@@ -87,15 +93,15 @@ class HistoryEntry(models.Model):
@cached_property
def is_change(self):
- return self.type == HistoryType.change
+ return self.type == HistoryType.change
@cached_property
def is_create(self):
- return self.type == HistoryType.create
+ return self.type == HistoryType.create
@cached_property
def is_delete(self):
- return self.type == HistoryType.delete
+ return self.type == HistoryType.delete
@property
def owner(self):
@@ -115,6 +121,20 @@ class HistoryEntry(models.Model):
self._owner = owner
self._prefetched_owner = True
+ def attach_user_info_to_comment_versions(self):
+ if not self.comment_versions:
+ return
+
+ from taiga.users.serializers import UserSerializer
+
+ user_ids = [v["user"]["id"] for v in self.comment_versions if "user" in v and "id" in v["user"]]
+ users_by_id = {u.id: u for u in get_user_model().objects.filter(id__in=user_ids)}
+
+ for version in self.comment_versions:
+ user = users_by_id.get(version["user"]["id"], None)
+ if user:
+ version["user"] = UserSerializer(user).data
+
@cached_property
def values_diff(self):
result = {}
@@ -166,7 +186,7 @@ class HistoryEntry(models.Model):
role_name = resolve_value("roles", role_id)
oldpoint_id = pointsold.get(role_id, None)
points[role_name] = [resolve_value("points", oldpoint_id),
- resolve_value("points", point_id)]
+ resolve_value("points", point_id)]
# Process that removes points entries with
# duplicate value.
@@ -185,8 +205,8 @@ class HistoryEntry(models.Model):
"deleted": [],
}
- oldattachs = {x["id"]:x for x in self.diff["attachments"][0]}
- newattachs = {x["id"]:x for x in self.diff["attachments"][1]}
+ oldattachs = {x["id"]: x for x in self.diff["attachments"][0]}
+ newattachs = {x["id"]: x for x in self.diff["attachments"][1]}
for aid in set(tuple(oldattachs.keys()) + tuple(newattachs.keys())):
if aid in oldattachs and aid in newattachs:
@@ -216,8 +236,8 @@ class HistoryEntry(models.Model):
"deleted": [],
}
- oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0] or []}
- newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1] or []}
+ oldcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][0] or []}
+ newcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][1] or []}
for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())):
if aid in oldcustattrs and aid in newcustattrs:
@@ -238,6 +258,24 @@ class HistoryEntry(models.Model):
if custom_attributes["new"] or custom_attributes["changed"] or custom_attributes["deleted"]:
value = custom_attributes
+ elif key == "user_stories":
+ user_stories = {
+ "new": [],
+ "deleted": [],
+ }
+
+ olduss = {x["id"]: x for x in self.diff["user_stories"][0]}
+ newuss = {x["id"]: x for x in self.diff["user_stories"][1]}
+
+ for usid in set(tuple(olduss.keys()) + tuple(newuss.keys())):
+ if usid in olduss and usid not in newuss:
+ user_stories["deleted"].append(olduss[usid])
+ elif usid not in olduss and usid in newuss:
+ user_stories["new"].append(newuss[usid])
+
+ if user_stories["new"] or user_stories["deleted"]:
+ value = user_stories
+
elif key in self.values:
value = [resolve_value(key, x) for x in self.diff[key]]
else:
diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py
index 9acbf3e1..40e4f98b 100644
--- a/taiga/projects/history/permissions.py
+++ b/taiga/projects/history/permissions.py
@@ -20,7 +20,7 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsProjectAdmin, AllowAny,
IsObjectOwner, PermissionComponent)
-from taiga.permissions.service import is_project_admin
+from taiga.permissions.services import is_project_admin
from taiga.projects.history.services import get_model_from_key, get_pk_from_key
@@ -34,32 +34,49 @@ class IsCommentOwner(PermissionComponent):
return obj.user and obj.user.get("pk", "not-pk") == request.user.pk
-class IsCommentProjectOwner(PermissionComponent):
+class IsCommentProjectAdmin(PermissionComponent):
def check_permissions(self, request, view, obj=None):
model = get_model_from_key(obj.key)
pk = get_pk_from_key(obj.key)
project = model.objects.get(pk=pk)
return is_project_admin(request.user, project)
+
+class EpicHistoryPermission(TaigaResourcePermission):
+ retrieve_perms = HasProjectPerm('view_project')
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
+
+
class UserStoryHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class TaskHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class IssueHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class WikiHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
- delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner()
- undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter()
+ edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
+ undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
+ comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py
index fe75f11d..0f2dc658 100644
--- a/taiga/projects/history/serializers.py
+++ b/taiga/projects/history/serializers.py
@@ -17,31 +17,38 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.base.fields import JsonField, I18NJsonField
+from taiga.base.fields import I18NJsonField, Field, MethodField
-from taiga.users.services import get_photo_or_gravatar_url
-
-from . import models
+from taiga.users.services import get_user_photo_url
+from taiga.users.gravatar import get_user_gravatar_id
-HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type")
+HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type")
-class HistoryEntrySerializer(serializers.ModelSerializer):
- diff = JsonField()
- snapshot = JsonField()
- values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
- values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
- user = serializers.SerializerMethodField("get_user")
- delete_comment_user = JsonField()
-
- class Meta:
- model = models.HistoryEntry
+class HistoryEntrySerializer(serializers.LightSerializer):
+ id = Field()
+ user = MethodField()
+ created_at = Field()
+ type = Field()
+ key = Field()
+ diff = Field()
+ snapshot = Field()
+ values = Field()
+ values_diff = I18NJsonField()
+ comment = I18NJsonField()
+ comment_html = Field()
+ delete_comment_date = Field()
+ delete_comment_user = Field()
+ edit_comment_date = Field()
+ is_hidden = Field()
+ is_snapshot = Field()
def get_user(self, entry):
user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False}
user.update(entry.user)
- user["photo"] = get_photo_or_gravatar_url(entry.owner)
+ user["photo"] = get_user_photo_url(entry.owner)
+ user["gravatar_id"] = get_user_gravatar_id(entry.owner)
if entry.owner:
user["is_active"] = entry.owner.is_active
diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py
index 0044bf53..1bf27dee 100644
--- a/taiga/projects/history/services.py
+++ b/taiga/projects/history/services.py
@@ -34,12 +34,9 @@ from collections import namedtuple
from copy import deepcopy
from functools import partial
from functools import wraps
-from functools import lru_cache
from django.conf import settings
from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
-from django.core.paginator import Paginator, InvalidPage
from django.apps import apps
from django.db import transaction as tx
from django_pglocks import advisory_lock
@@ -50,6 +47,25 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
from .models import HistoryType
+# Freeze implementatitions
+from .freeze_impl import project_freezer
+from .freeze_impl import milestone_freezer
+from .freeze_impl import epic_freezer
+from .freeze_impl import epic_related_userstory_freezer
+from .freeze_impl import userstory_freezer
+from .freeze_impl import issue_freezer
+from .freeze_impl import task_freezer
+from .freeze_impl import wikipage_freezer
+
+
+from .freeze_impl import project_values
+from .freeze_impl import milestone_values
+from .freeze_impl import epic_values
+from .freeze_impl import epic_related_userstory_values
+from .freeze_impl import userstory_values
+from .freeze_impl import issue_values
+from .freeze_impl import task_values
+from .freeze_impl import wikipage_values
# Type that represents a freezed object
FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"])
@@ -64,6 +80,7 @@ _values_impl_map = {}
# Not important fields for models (history entries with only
# this fields are marked as hidden).
_not_important_fields = {
+ "epics.epic": frozenset(["epics_order", "user_stories"]),
"userstories.userstory": frozenset(["backlog_order", "sprint_order", "kanban_order"]),
"tasks.task": frozenset(["us_order", "taskboard_order"]),
}
@@ -71,7 +88,7 @@ _not_important_fields = {
log = logging.getLogger("taiga.history")
-def make_key_from_model_object(obj:object) -> str:
+def make_key_from_model_object(obj: object) -> str:
"""
Create unique key from model instance.
"""
@@ -79,7 +96,7 @@ def make_key_from_model_object(obj:object) -> str:
return "{0}:{1}".format(tn, obj.pk)
-def get_model_from_key(key:str) -> object:
+def get_model_from_key(key: str) -> object:
"""
Get model from key
"""
@@ -87,7 +104,7 @@ def get_model_from_key(key:str) -> object:
return apps.get_model(class_name)
-def get_pk_from_key(key:str) -> object:
+def get_pk_from_key(key: str) -> object:
"""
Get pk from key
"""
@@ -95,7 +112,21 @@ def get_pk_from_key(key:str) -> object:
return pk
-def register_values_implementation(typename:str, fn=None):
+def get_instance_from_key(key: str) -> object:
+ """
+ Get instance from key
+ """
+ model = get_model_from_key(key)
+ pk = get_pk_from_key(key)
+ try:
+ obj = model.objects.get(pk=pk)
+ return obj
+ except model.DoesNotExist:
+ # Catch simultaneous DELETE request
+ return None
+
+
+def register_values_implementation(typename: str, fn=None):
"""
Register values implementation for specified typename.
This function can be used as decorator.
@@ -114,7 +145,7 @@ def register_values_implementation(typename:str, fn=None):
return _wrapper
-def register_freeze_implementation(typename:str, fn=None):
+def register_freeze_implementation(typename: str, fn=None):
"""
Register freeze implementation for specified typename.
This function can be used as decorator.
@@ -135,7 +166,7 @@ def register_freeze_implementation(typename:str, fn=None):
# Low level api
-def freeze_model_instance(obj:object) -> FrozenObj:
+def freeze_model_instance(obj: object) -> FrozenObj:
"""
Creates a new frozen object from model instance.
@@ -165,7 +196,7 @@ def freeze_model_instance(obj:object) -> FrozenObj:
return FrozenObj(key, snapshot)
-def is_hidden_snapshot(obj:FrozenDiff) -> bool:
+def is_hidden_snapshot(obj: FrozenDiff) -> bool:
"""
Check if frozen object is considered
hidden or not.
@@ -185,7 +216,7 @@ def is_hidden_snapshot(obj:FrozenDiff) -> bool:
return False
-def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff:
+def make_diff(oldobj: FrozenObj, newobj: FrozenObj) -> FrozenDiff:
"""
Compute a diff between two frozen objects.
"""
@@ -203,7 +234,7 @@ def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff:
return FrozenDiff(newobj.key, diff, newobj.snapshot)
-def make_diff_values(typename:str, fdiff:FrozenDiff) -> dict:
+def make_diff_values(typename: str, fdiff: FrozenDiff) -> dict:
"""
Given a typename and diff, build a values dict for it.
If no implementation found for typename, warnig is raised in
@@ -228,7 +259,7 @@ def _rebuild_snapshot_from_diffs(keysnapshot, partials):
return result
-def get_last_snapshot_for_key(key:str) -> FrozenObj:
+def get_last_snapshot_for_key(key: str) -> FrozenObj:
entry_model = apps.get_model("history", "HistoryEntry")
# Search last snapshot
@@ -257,17 +288,16 @@ def get_last_snapshot_for_key(key:str) -> FrozenObj:
# Public api
-def get_modified_fields(obj:object, last_modifications):
+def get_modified_fields(obj: object, last_modifications):
"""
Get the modified fields for an object through his last modifications
"""
key = make_key_from_model_object(obj)
entry_model = apps.get_model("history", "HistoryEntry")
history_entries = (entry_model.objects
- .filter(key=key)
- .order_by("-created_at")
- .values_list("diff", flat=True)
- [0:last_modifications])
+ .filter(key=key)
+ .order_by("-created_at")
+ .values_list("diff", flat=True)[0:last_modifications])
modified_fields = []
for history_entry in history_entries:
@@ -277,7 +307,7 @@ def get_modified_fields(obj:object, last_modifications):
@tx.atomic
-def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
+def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False):
"""
Given any model instance with registred content type,
create new history entry of "change" type.
@@ -287,7 +317,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
"""
key = make_key_from_model_object(obj)
- with advisory_lock(key) as acquired_key_lock:
+ with advisory_lock("history-"+key):
typename = get_typename_for_model_class(obj.__class__)
new_fobj = freeze_model_instance(obj)
@@ -300,6 +330,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# Determine history type
if delete:
entry_type = HistoryType.delete
+ need_real_snapshot = True
elif new_fobj and not old_fobj:
entry_type = HistoryType.create
elif new_fobj and old_fobj:
@@ -311,10 +342,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# If diff and comment are empty, do
# not create empty history entry
- if (not fdiff.diff and not comment
- and old_fobj is not None
- and entry_type != HistoryType.delete):
-
+ if (not fdiff.diff and not comment and old_fobj is not None and entry_type != HistoryType.delete):
return None
fvals = make_diff_values(typename, fdiff)
@@ -326,6 +354,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
kwargs = {
"user": {"pk": user_id, "name": user_name},
+ "project_id": getattr(obj, 'project_id', getattr(obj, 'id', None)),
"key": key,
"type": entry_type,
"snapshot": fdiff.snapshot if need_real_snapshot else None,
@@ -342,7 +371,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# High level query api
-def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,),
+def get_history_queryset_by_model_instance(obj: object, types=(HistoryType.change,),
include_hidden=False):
"""
Get one page of history for specified object.
@@ -361,36 +390,26 @@ def prefetch_owners_in_history_queryset(qs):
user_ids = [u["pk"] for u in qs.values_list("user", flat=True)]
users = get_user_model().objects.filter(id__in=user_ids)
users_by_id = {u.id: u for u in users}
- for history_entry in qs:
+ for history_entry in qs:
history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None))
return qs
-# Freeze implementatitions
-from .freeze_impl import project_freezer
-from .freeze_impl import milestone_freezer
-from .freeze_impl import userstory_freezer
-from .freeze_impl import issue_freezer
-from .freeze_impl import task_freezer
-from .freeze_impl import wikipage_freezer
-
+# Freeze & value register
register_freeze_implementation("projects.project", project_freezer)
register_freeze_implementation("milestones.milestone", milestone_freezer,)
+register_freeze_implementation("epics.epic", epic_freezer)
+register_freeze_implementation("epics.relateduserstory", epic_related_userstory_freezer)
register_freeze_implementation("userstories.userstory", userstory_freezer)
register_freeze_implementation("issues.issue", issue_freezer)
register_freeze_implementation("tasks.task", task_freezer)
register_freeze_implementation("wiki.wikipage", wikipage_freezer)
-from .freeze_impl import project_values
-from .freeze_impl import milestone_values
-from .freeze_impl import userstory_values
-from .freeze_impl import issue_values
-from .freeze_impl import task_values
-from .freeze_impl import wikipage_values
-
register_values_implementation("projects.project", project_values)
register_values_implementation("milestones.milestone", milestone_values)
+register_values_implementation("epics.epic", epic_values)
+register_values_implementation("epics.relateduserstory", epic_related_userstory_values)
register_values_implementation("userstories.userstory", userstory_values)
register_values_implementation("issues.issue", issue_values)
register_values_implementation("tasks.task", task_values)
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index b368ec8d..617b13ec 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -27,21 +27,25 @@ from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
+from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
-from taiga.projects.history.mixins import HistoryResourceMixin
-
-from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
+from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
+from .utils import attach_extra_info
+
from . import models
from . import services
from . import permissions
from . import serializers
+from . import validators
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- BlockedByProjectMixin, ModelCrudViewSet):
+ TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
+ validator_class = validators.IssueValidator
queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend,
@@ -54,19 +58,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
filters.TagsFilter,
filters.WatchersFilter,
filters.QFilter,
+ filters.CreatedDateFilter,
+ filters.ModifiedDateFilter,
+ filters.FinishedDateFilter,
filters.OrderByFilterMixin)
- retrieve_exclude_filters = (filters.OwnersFilter,
- filters.AssignedToFilter,
- filters.StatusesFilter,
- filters.IssueTypesFilter,
- filters.SeveritiesFilter,
- filters.PrioritiesFilter,
- filters.TagsFilter,
- filters.WatchersFilter,)
-
filter_fields = ("project",
+ "project__slug",
"status__is_closed")
-
order_by_fields = ("type",
"status",
"severity",
@@ -143,10 +141,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def get_queryset(self):
qs = super().get_queryset()
- qs = qs.prefetch_related("attachments", "generated_user_stories")
qs = qs.select_related("owner", "assigned_to", "status", "project")
- qs = self.attach_votes_attrs_to_queryset(qs)
- return self.attach_watchers_attrs_to_queryset(qs)
+ qs = attach_extra_info(qs, user=self.request.user)
+ return qs
def pre_save(self, obj):
if not obj.id:
@@ -179,10 +176,18 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
@list_route(methods=["GET"])
def by_ref(self, request):
- ref = request.QUERY_PARAMS.get("ref", None)
+ retrieve_kwargs = {
+ "ref": request.QUERY_PARAMS.get("ref", None)
+ }
project_id = request.QUERY_PARAMS.get("project", None)
- issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id)
- return self.retrieve(request, pk=issue.pk)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"])
def filters_data(self, request, *args, **kwargs):
@@ -196,7 +201,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
priorities_filter_backends = (f for f in filter_backends if f != filters.PrioritiesFilter)
severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter)
- tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter)
queryset = self.get_queryset()
querysets = {
@@ -225,9 +229,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.IssuesBulkSerializer(data=request.DATA)
- if serializer.is_valid():
- data = serializer.data
+ validator = validators.IssuesBulkValidator(data=request.DATA)
+ if validator.is_valid():
+ data = validator.data
project = Project.objects.get(pk=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None:
@@ -238,11 +242,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
status=project.default_issue_status, severity=project.default_severity,
priority=project.default_priority, type=project.default_issue_type,
callback=self.post_save, precall=self.pre_save)
+
+ issues = self.get_queryset().filter(id__in=[i.id for i in issues])
issues_serialized = self.get_serializer_class()(issues, many=True)
return response.Ok(data=issues_serialized.data)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py
index 4d0bca19..de4de986 100644
--- a/taiga/projects/issues/apps.py
+++ b/taiga/projects/issues/apps.py
@@ -22,7 +22,7 @@ from django.db.models import signals
def connect_issues_signals():
- from taiga.projects import signals as generic_handlers
+ from taiga.projects.tagging import signals as tagging_handlers
from . import signals as handlers
# Finished date
@@ -31,15 +31,9 @@ def connect_issues_signals():
dispatch_uid="set_finished_date_when_edit_issue")
# Tags
- signals.pre_save.connect(generic_handlers.tags_normalization,
+ signals.pre_save.connect(tagging_handlers.tags_normalization,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="tags_normalization_issue")
- signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
- sender=apps.get_model("issues", "Issue"),
- dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue")
- signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
- sender=apps.get_model("issues", "Issue"),
- dispatch_uid="update_project_tags_when_delete_taggable_item_issue")
def connect_issues_custom_attributes_signals():
@@ -56,14 +50,15 @@ def connect_all_issues_signals():
def disconnect_issues_signals():
- signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="set_finished_date_when_edit_issue")
- signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="tags_normalization_issue")
- signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue")
- signals.post_delete.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_delete_taggable_item_issue")
+ signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
+ dispatch_uid="set_finished_date_when_edit_issue")
+ signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
+ dispatch_uid="tags_normalization_issue")
def disconnect_issues_custom_attributes_signals():
- signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="create_custom_attribute_value_when_create_issue")
+ signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),
+ dispatch_uid="create_custom_attribute_value_when_create_issue")
def disconnect_all_issues_signals():
diff --git a/taiga/projects/issues/migrations/0007_auto_20160614_1201.py b/taiga/projects/issues/migrations/0007_auto_20160614_1201.py
new file mode 100644
index 00000000..f522d46c
--- /dev/null
+++ b/taiga/projects/issues/migrations/0007_auto_20160614_1201.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-14 12:01
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('issues', '0006_remove_issue_watchers'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='issue',
+ name='external_reference',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=False, null=False), blank=True, default=None, null=True, size=None, verbose_name='external reference'),
+ ),
+ migrations.AlterField(
+ model_name='issue',
+ name='tags',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
+ ),
+ ]
diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py
index 89a78051..8f9c18a3 100644
--- a/taiga/projects/issues/models.py
+++ b/taiga/projects/issues/models.py
@@ -18,19 +18,16 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.postgres.fields import ArrayField
from django.conf import settings
from django.utils import timezone
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
-from djorm_pgarray.fields import TextArrayField
-
from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.mixins.blocked import BlockedMixin
-from taiga.base.tags import TaggedMixin
-
-from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags
+from taiga.projects.tagging.models import TaggedMixin
class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
@@ -65,7 +62,8 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.
default=None, related_name="issues_assigned_to_me",
verbose_name=_("assigned to"))
attachments = GenericRelation("attachments.Attachment")
- external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
+ external_reference = ArrayField(models.TextField(null=False, blank=False),
+ null=True, blank=True, default=None, verbose_name=_("external reference"))
_importing = None
class Meta:
diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py
index efb53267..d86f697c 100644
--- a/taiga/projects/issues/permissions.py
+++ b/taiga/projects/issues/permissions.py
@@ -17,9 +17,10 @@
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsProjectAdmin, PermissionComponent,
- AllowAny, IsAuthenticated, IsSuperUser)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
class IssuePermission(TaigaResourcePermission):
@@ -27,8 +28,8 @@ class IssuePermission(TaigaResourcePermission):
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
create_perms = HasProjectPerm('add_issue')
- update_perms = HasProjectPerm('modify_issue')
- partial_update_perms = HasProjectPerm('modify_issue')
+ update_perms = CommentAndOrUpdatePerm('modify_issue', 'comment_issue')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_issue', 'comment_issue')
destroy_perms = HasProjectPerm('delete_issue')
list_perms = AllowAny()
filters_data_perms = AllowAny()
@@ -41,14 +42,6 @@ class IssuePermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues')
-class HasIssueIdUrlParam(PermissionComponent):
- def check_permissions(self, request, view, obj=None):
- param = view.kwargs.get('issue_id', None)
- if param:
- return True
- return False
-
-
class IssueVotersPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
global_perms = None
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index 6c2f877e..a76fbf7d 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -17,48 +17,53 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.base.fields import TagsField
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender
-from taiga.projects.validators import ProjectExistsValidator
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.serializers import BasicIssueStatusSerializer
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
+from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
-from taiga.users.serializers import UserBasicInfoSerializer
-from . import models
+class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin,
+ TaggedInProjectResourceSerializer, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ severity = Field(attr="severity_id")
+ priority = Field(attr="priority_id")
+ type = Field(attr="type_id")
+ milestone = Field(attr="milestone_id")
+ project = Field(attr="project_id")
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ external_reference = Field()
+ version = Field()
+ watchers = Field()
+ is_closed = Field()
-class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
- tags = TagsField(required=False)
- external_reference = PgArrayField(required=False)
- is_closed = serializers.Field(source="is_closed")
- comment = serializers.SerializerMethodField("get_comment")
- generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
- blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
- description_html = serializers.SerializerMethodField("get_description_html")
- status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True)
- assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
- owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
-
- class Meta:
- model = models.Issue
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+class IssueSerializer(IssueListSerializer):
+ comment = MethodField()
+ generated_user_stories = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work
return ""
def get_generated_user_stories(self, obj):
- return [{
- "id": us.id,
- "ref": us.ref,
- "subject": us.subject,
- } for us in obj.generated_user_stories.all()]
+ assert hasattr(obj, "generated_user_stories_attr"), "instance must have a generated_user_stories_attr attribute"
+ return obj.generated_user_stories_attr
def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note)
@@ -67,34 +72,5 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
return mdrender(obj.project, obj.description)
-class IssueListSerializer(IssueSerializer):
- class Meta:
- model = models.Issue
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
- exclude=("description", "description_html")
-
-
-class IssueListSerializer(IssueSerializer):
- class Meta:
- model = models.Issue
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
- exclude=("description", "description_html")
-
-
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
- def serialize_neighbor(self, neighbor):
- if neighbor:
- return NeighborIssueSerializer(neighbor).data
- return None
-
-
-class NeighborIssueSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Issue
- fields = ("id", "ref", "subject")
- depth = 0
-
-
-class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_issues = serializers.CharField()
+ pass
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index a494b1f4..ad87a61e 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -35,6 +35,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset
from . import models
+#####################################################
+# Bulk actions
+#####################################################
+
def get_issues_from_bulk(bulk_data, **additional_fields):
"""Convert `bulk_data` into a list of issues.
@@ -68,20 +72,9 @@ def create_issues_in_bulk(bulk_data, callback=None, precall=None, **additional_f
return issues
-def update_issues_order_in_bulk(bulk_data):
- """Update the order of some issues.
-
- `bulk_data` should be a list of tuples with the following format:
-
- [(, ), ...]
- """
- issue_ids = []
- new_order_values = []
- for issue_id, new_order_value in bulk_data:
- issue_ids.append(issue_id)
- new_order_values.append({"order": new_order_value})
- db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue)
-
+#####################################################
+# CSV
+#####################################################
def issues_to_csv(project, queryset):
csv_data = io.StringIO()
@@ -143,6 +136,10 @@ def issues_to_csv(project, queryset):
return csv_data
+#####################################################
+# Api filter data
+#####################################################
+
def _get_issues_statuses(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
@@ -394,7 +391,7 @@ def _get_issues_owners(project, queryset):
FROM projects_membership
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
- WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
+ WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
-- System users
UNION
@@ -423,16 +420,49 @@ def _get_issues_owners(project, queryset):
return sorted(result, key=itemgetter("full_name"))
-def _get_issues_tags(queryset):
- tags = []
- for t_list in queryset.values_list("tags", flat=True):
- if t_list is None:
- continue
- tags += list(t_list)
+def _get_issues_tags(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
- tags = [{"name":e, "count":tags.count(e)} for e in set(tags)]
+ extra_sql = """
+ WITH "issues_tags" AS (
+ SELECT "tag",
+ COUNT("tag") "counter"
+ FROM (
+ SELECT UNNEST("issues_issue"."tags") "tag"
+ FROM "issues_issue"
+ INNER JOIN "projects_project"
+ ON ("issues_issue"."project_id" = "projects_project"."id")
+ WHERE {where}
+ ) "tags"
+ GROUP BY "tag"),
+ "project_tags" AS (
+ SELECT reduce_dim("tags_colors") "tag_color"
+ FROM "projects_project"
+ WHERE "id"=%s)
- return sorted(tags, key=itemgetter("name"))
+ SELECT "tag_color"[1] "tag",
+ "tag_color"[2] "color",
+ COALESCE("issues_tags"."counter", 0) "counter"
+ FROM project_tags
+ LEFT JOIN "issues_tags" ON "project_tags"."tag_color"[1] = "issues_tags"."tag"
+ ORDER BY "tag"
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for name, color, count in rows:
+ result.append({
+ "name": name,
+ "color": color,
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("name"))
def get_issues_filters_data(project, querysets):
@@ -447,7 +477,7 @@ def get_issues_filters_data(project, querysets):
("severities", _get_issues_severities(project, querysets["severities"])),
("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])),
("owners", _get_issues_owners(project, querysets["owners"])),
- ("tags", _get_issues_tags(querysets["tags"])),
+ ("tags", _get_issues_tags(project, querysets["tags"])),
])
return data
diff --git a/taiga/projects/issues/utils.py b/taiga/projects/issues/utils.py
new file mode 100644
index 00000000..2053d923
--- /dev/null
+++ b/taiga/projects/issues/utils.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_generated_user_stories(queryset, as_field="generated_user_stories_attr"):
+ """Attach generated user stories json column to each object of the queryset.
+
+ :param queryset: A Django issues queryset object.
+ :param as_field: Attach the generated user stories as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ userstories_userstory.id,
+ userstories_userstory.ref,
+ userstories_userstory.subject
+ FROM userstories_userstory
+ WHERE generated_from_issue_id = {tbl}.id) t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None):
+ queryset = attach_generated_user_stories(queryset)
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/issues/validators.py b/taiga/projects/issues/validators.py
new file mode 100644
index 00000000..4c900c15
--- /dev/null
+++ b/taiga/projects/issues/validators.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.fields import PgArrayField
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.validators import ProjectExistsValidator
+
+from . import models
+
+
+class IssueValidator(WatchersValidator, EditableWatchedResourceSerializer,
+ validators.ModelValidator):
+
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+
+ class Meta:
+ model = models.Issue
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+
+class IssuesBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_issues = serializers.CharField()
diff --git a/taiga/projects/likes/serializers.py b/taiga/projects/likes/serializers.py
index 6a654705..ef058e70 100644
--- a/taiga/projects/likes/serializers.py
+++ b/taiga/projects/likes/serializers.py
@@ -17,14 +17,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.contrib.auth import get_user_model
-
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
-class FanSerializer(serializers.ModelSerializer):
- full_name = serializers.CharField(source='get_full_name', required=False)
+class FanSerializer(serializers.LightSerializer):
+ id = Field()
+ username = Field()
+ full_name = MethodField()
- class Meta:
- model = get_user_model()
- fields = ('id', 'username', 'full_name')
+ def get_full_name(self, obj):
+ return obj.get_full_name()
diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py
index 064142d0..e8b47290 100644
--- a/taiga/projects/management/commands/sample_data.py
+++ b/taiga/projects/management/commands/sample_data.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 .
-import random
import datetime
from os import path
+from hashlib import sha1
from django.core.management.base import BaseCommand
@@ -30,9 +30,11 @@ from django.contrib.contenttypes.models import ContentType
from sampledatahelper.helper import SampleDataHelper
from taiga.users.models import *
-from taiga.permissions.permissions import ANON_PERMISSIONS
+from taiga.permissions.choices import ANON_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_STAFF
+from taiga.external_apps.models import Application, ApplicationToken
from taiga.projects.models import *
+from taiga.projects.epics.models import *
from taiga.projects.milestones.models import *
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.services.stats import get_stats_for_project
@@ -108,15 +110,20 @@ NUM_PROJECTS =getattr(settings, "SAMPLE_DATA_NUM_PROJECTS", 4)
NUM_EMPTY_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_EMPTY_PROJECTS", 2)
NUM_BLOCKED_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_BLOCKED_PROJECTS", 1)
NUM_MILESTONES = getattr(settings, "SAMPLE_DATA_NUM_MILESTONES", (1, 5))
+NUM_EPICS = getattr(settings, "SAMPLE_DATA_NUM_EPICS", (4, 8))
+NUM_USS_EPICS = getattr(settings, "SAMPLE_DATA_NUM_USS_EPICS", (2, 12))
NUM_USS = getattr(settings, "SAMPLE_DATA_NUM_USS", (3, 7))
NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5))
NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4))
NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20))
NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25))
-NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4))
+NUM_WIKI_LINKS = getattr(settings, "SAMPLE_DATA_NUM_WIKI_LINKS", (0, 15))
+NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (1, 4))
NUM_LIKES = getattr(settings, "SAMPLE_DATA_NUM_LIKES", (0, 10))
NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 10))
NUM_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 8))
+NUM_APPLICATIONS = getattr(settings, "SAMPLE_DATA_NUM_APPLICATIONS", (1, 3))
+NUM_APPLICATIONS_TOKENS = getattr(settings, "SAMPLE_DATA_NUM_APPLICATIONS_TOKENS", (1, 3))
FEATURED_PROJECTS_POSITIONS = [0, 1, 2]
LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS = [0, 1, 2]
@@ -124,7 +131,7 @@ LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS = [0, 1, 2]
class Command(BaseCommand):
sd = SampleDataHelper(seed=12345678901)
- @transaction.atomic
+ #@transaction.atomic
def handle(self, *args, **options):
# Prevent events emission when sample data is running
disconnect_events_signals()
@@ -179,36 +186,43 @@ class Command(BaseCommand):
project=project,
role=role,
is_admin=self.sd.boolean(),
- token=''.join(random.sample('abcdef0123456789', 10)))
+ token=self.sd.hex_chars(10,10))
if role.computable:
computable_project_roles.add(role)
- # added custom attributes
- if self.sd.boolean:
- for i in range(1, 4):
- UserStoryCustomAttribute.objects.create(name=self.sd.words(1, 3),
- description=self.sd.words(3, 12),
- type=self.sd.choice(TYPES_CHOICES)[0],
- project=project,
- order=i)
- if self.sd.boolean:
- for i in range(1, 4):
- TaskCustomAttribute.objects.create(name=self.sd.words(1, 3),
+ # If the project isn't empty
+ if x not in empty_projects_range:
+ # added custom attributes
+ names = set([self.sd.words(1, 3) for i in range(1, 6)])
+ for name in names:
+ EpicCustomAttribute.objects.create(name=name,
description=self.sd.words(3, 12),
type=self.sd.choice(TYPES_CHOICES)[0],
project=project,
order=i)
- if self.sd.boolean:
- for i in range(1, 4):
- IssueCustomAttribute.objects.create(name=self.sd.words(1, 3),
+ names = set([self.sd.words(1, 3) for i in range(1, 6)])
+ for name in names:
+ UserStoryCustomAttribute.objects.create(name=name,
+ description=self.sd.words(3, 12),
+ type=self.sd.choice(TYPES_CHOICES)[0],
+ project=project,
+ order=i)
+ names = set([self.sd.words(1, 3) for i in range(1, 6)])
+ for name in names:
+ TaskCustomAttribute.objects.create(name=name,
+ description=self.sd.words(3, 12),
+ type=self.sd.choice(TYPES_CHOICES)[0],
+ project=project,
+ order=i)
+ names = set([self.sd.words(1, 3) for i in range(1, 6)])
+ for name in names:
+ IssueCustomAttribute.objects.create(name=name,
description=self.sd.words(3, 12),
type=self.sd.choice(TYPES_CHOICES)[0],
project=project,
order=i)
- # If the project isn't empty
- if x not in empty_projects_range:
start_date = now() - datetime.timedelta(55)
# create milestones
@@ -243,8 +257,24 @@ class Command(BaseCommand):
for y in range(self.sd.int(*NUM_ISSUES)):
bug = self.create_bug(project)
- # create a wiki page
- wiki_page = self.create_wiki(project, "home")
+ # create a wiki pages and wiki links
+ wiki_page = self.create_wiki_page(project, "home")
+
+ for y in range(self.sd.int(*NUM_WIKI_LINKS)):
+ wiki_link = self.create_wiki_link(project)
+ if self.sd.boolean():
+ self.create_wiki_page(project, wiki_link.href)
+
+ # create epics
+ for y in range(self.sd.int(*NUM_EPICS)):
+ epic = self.create_epic(project)
+
+ project.refresh_from_db()
+
+ # Set color for some tags:
+ for tag in project.tags_colors:
+ if self.sd.boolean():
+ tag[1] = self.generate_color(tag[0])
# Set a value to total_story_points to show the deadline in the backlog
project_stats = get_stats_for_project(project)
@@ -270,7 +300,14 @@ class Command(BaseCommand):
attached_file=attached_file)
return attachment
- def create_wiki(self, project, slug):
+
+ def create_wiki_link(self, project, title=None):
+ wiki_link = WikiLink.objects.create(project=project,
+ title=title or self.sd.words(1, 3))
+ return wiki_link
+
+
+ def create_wiki_page(self, project, slug):
wiki_page = WikiPage.objects.create(project=project,
slug=slug,
content=self.sd.paragraphs(3,15),
@@ -323,7 +360,7 @@ class Command(BaseCommand):
bug.save()
custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca
- in project.issuecustomattributes.all() if self.sd.boolean()}
+ in project.issuecustomattributes.all().order_by('id') if self.sd.boolean()}
if custom_attributes_values:
bug.custom_attributes_values.attributes_values = custom_attributes_values
bug.custom_attributes_values.save()
@@ -375,7 +412,7 @@ class Command(BaseCommand):
task.save()
custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca
- in project.taskcustomattributes.all() if self.sd.boolean()}
+ in project.taskcustomattributes.all().order_by('id') if self.sd.boolean()}
if custom_attributes_values:
task.custom_attributes_values.attributes_values = custom_attributes_values
task.custom_attributes_values.save()
@@ -423,7 +460,7 @@ class Command(BaseCommand):
us.save()
custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca
- in project.userstorycustomattributes.all() if self.sd.boolean()}
+ in project.userstorycustomattributes.all().order_by('id') if self.sd.boolean()}
if custom_attributes_values:
us.custom_attributes_values.attributes_values = custom_attributes_values
us.custom_attributes_values.save()
@@ -470,6 +507,63 @@ class Command(BaseCommand):
return milestone
+ def create_epic(self, project):
+ epic = Epic.objects.create(subject=self.sd.choice(SUBJECT_CHOICES),
+ project=project,
+ owner=self.sd.db_object_from_queryset(
+ project.memberships.filter(user__isnull=False)).user,
+ description=self.sd.paragraph(),
+ status=self.sd.db_object_from_queryset(project.epic_statuses.filter(
+ is_closed=False)),
+ tags=self.sd.words(1, 3).split(" "))
+ epic.save()
+
+ custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca
+ in project.epiccustomattributes.all().order_by("id") if self.sd.boolean()}
+ if custom_attributes_values:
+ epic.custom_attributes_values.attributes_values = custom_attributes_values
+ epic.custom_attributes_values.save()
+
+ for i in range(self.sd.int(*NUM_ATTACHMENTS)):
+ attachment = self.create_attachment(epic, i+1)
+
+ if self.sd.choice([True, True, False, True, True]):
+ epic.assigned_to = self.sd.db_object_from_queryset(project.memberships.filter(
+ user__isnull=False)).user
+ epic.save()
+
+ take_snapshot(epic,
+ comment=self.sd.paragraph(),
+ user=epic.owner)
+
+ # Add history entry
+ epic.status=self.sd.db_object_from_queryset(project.epic_statuses.filter(is_closed=False))
+ epic.save()
+ take_snapshot(epic,
+ comment=self.sd.paragraph(),
+ user=epic.owner)
+
+ self.create_votes(epic)
+ self.create_watchers(epic)
+
+ if self.sd.choice([True, True, False, True, True]):
+ filters = {}
+ if self.sd.choice([True, True, False, True, True]):
+ filters = {"project": epic.project}
+ n = self.sd.choice(list(range(self.sd.int(*NUM_USS_EPICS))))
+ user_stories = UserStory.objects.filter(**filters).order_by("?")[:n]
+ for idx, us in enumerate(list(user_stories)):
+ RelatedUserStory.objects.create(epic=epic,
+ user_story=us,
+ order=idx+1)
+
+ # Add history entry
+ take_snapshot(epic,
+ comment=self.sd.paragraph(),
+ user=epic.owner)
+
+ return epic
+
def create_project(self, counter, is_private=None, blocked_code=None):
if is_private is None:
is_private=self.sd.boolean()
@@ -479,7 +573,7 @@ class Command(BaseCommand):
project = Project.objects.create(slug='project-%s'%(counter),
name='Project Example {0}'.format(counter),
description='Project example {0} description'.format(counter),
- owner=random.choice(self.users),
+ owner=self.sd.choice(self.users),
is_private=is_private,
anon_permissions=anon_permissions,
public_permissions=public_permissions,
@@ -492,6 +586,7 @@ class Command(BaseCommand):
blocked_code=blocked_code)
project.is_kanban_activated = True
+ project.is_epics_activated = True
project.save()
take_snapshot(project, user=project.owner)
@@ -509,7 +604,7 @@ class Command(BaseCommand):
user = User.objects.create(username=username,
full_name=full_name,
email=email,
- token=''.join(random.sample('abcdef0123456789', 10)),
+ token=self.sd.hex_chars(10,10),
color=self.sd.choice(COLOR_CHOICES))
user.set_password('123123')
@@ -534,3 +629,8 @@ class Command(BaseCommand):
obj.add_watcher(user)
else:
obj.add_watcher(user, notify_level)
+
+ def generate_color(self, tag):
+ color = sha1(tag.encode("utf-8")).hexdigest()[0:6]
+ return "#{}".format(color)
+
diff --git a/taiga/projects/migrations/0030_auto_20151128_0757.py b/taiga/projects/migrations/0030_auto_20151128_0757.py
index 5f515029..425598e7 100644
--- a/taiga/projects/migrations/0030_auto_20151128_0757.py
+++ b/taiga/projects/migrations/0030_auto_20151128_0757.py
@@ -110,7 +110,9 @@ class Migration(migrations.Migration):
dependencies = [
('projects', '0029_project_is_looking_for_people'),
+ ('likes', '0001_initial'),
('timeline', '0004_auto_20150603_1312'),
+ ('likes', '0001_initial'),
]
operations = [
diff --git a/taiga/projects/migrations/0041_auto_20160519_1058.py b/taiga/projects/migrations/0041_auto_20160519_1058.py
new file mode 100644
index 00000000..c4b0a2fd
--- /dev/null
+++ b/taiga/projects/migrations/0041_auto_20160519_1058.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-19 10:58
+from __future__ import unicode_literals
+
+from django.db import migrations
+import djorm_pgarray.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0040_remove_memberships_of_cancelled_users_acounts'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='public_permissions',
+ field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', default=[], verbose_name='user permissions'),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0042_auto_20160525_0911.py b/taiga/projects/migrations/0042_auto_20160525_0911.py
new file mode 100644
index 00000000..0652df06
--- /dev/null
+++ b/taiga/projects/migrations/0042_auto_20160525_0911.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-25 09:11
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+UPDATE_PROJECTS_ANON_PERMISSIONS_SQL = """
+ UPDATE projects_project
+ SET
+ ANON_PERMISSIONS = array_append(ANON_PERMISSIONS, '{comment_permission}')
+ WHERE
+ '{base_permission}' = ANY(ANON_PERMISSIONS)
+ AND
+ NOT '{comment_permission}' = ANY(ANON_PERMISSIONS)
+"""
+
+UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL = """
+ UPDATE projects_project
+ SET
+ PUBLIC_PERMISSIONS = array_append(PUBLIC_PERMISSIONS, '{comment_permission}')
+ WHERE
+ '{base_permission}' = ANY(PUBLIC_PERMISSIONS)
+ AND
+ NOT '{comment_permission}' = ANY(PUBLIC_PERMISSIONS)
+"""
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0041_auto_20160519_1058'),
+ ]
+
+ operations = [
+ # user stories
+ migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format(
+ base_permission="modify_us",
+ comment_permission="comment_us")
+ ),
+
+ migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format(
+ base_permission="modify_us",
+ comment_permission="comment_us")
+ ),
+
+ # tasks
+ migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format(
+ base_permission="modify_task",
+ comment_permission="comment_task")
+ ),
+
+ migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format(
+ base_permission="modify_task",
+ comment_permission="comment_task")
+ ),
+
+ # issues
+ migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ ),
+
+ migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ )
+ ]
diff --git a/taiga/projects/migrations/0045_merge.py b/taiga/projects/migrations/0045_merge.py
new file mode 100644
index 00000000..09b3d419
--- /dev/null
+++ b/taiga/projects/migrations/0045_merge.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-31 11:59
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0044_auto_20160531_1150'),
+ ('projects', '0042_auto_20160525_0911'),
+ ]
+
+ operations = [
+ ]
diff --git a/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py
new file mode 100644
index 00000000..af7fbf8d
--- /dev/null
+++ b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-07 06:19
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0045_merge'),
+ ('userstories', '0011_userstory_tribe_gig'),
+ ('tasks', '0009_auto_20151104_1131'),
+ ('issues', '0006_remove_issue_watchers'),
+ ]
+
+ operations = [
+ # Function: Reduce a multidimensional array only on its first level
+ migrations.RunSQL(
+ """
+ CREATE OR REPLACE FUNCTION public.reduce_dim(anyarray)
+ RETURNS SETOF anyarray
+ AS $function$
+ DECLARE
+ s $1%TYPE;
+ BEGIN
+ IF $1 = '{}' THEN
+ RETURN;
+ END IF;
+ FOREACH s SLICE 1 IN ARRAY $1 LOOP
+ RETURN NEXT s;
+ END LOOP;
+ RETURN;
+ END;
+ $function$
+ LANGUAGE plpgsql IMMUTABLE;
+ """
+ ),
+ # Function: aggregates multi dimensional arrays
+ migrations.RunSQL(
+ """
+ DROP AGGREGATE IF EXISTS array_agg_mult (anyarray);
+ CREATE AGGREGATE array_agg_mult (anyarray) (
+ SFUNC = array_cat
+ ,STYPE = anyarray
+ ,INITCOND = '{}'
+ );
+ """
+ ),
+ # Function: array_distinct
+ migrations.RunSQL(
+ """
+ CREATE OR REPLACE FUNCTION array_distinct(anyarray)
+ RETURNS anyarray AS $$
+ SELECT ARRAY(SELECT DISTINCT unnest($1))
+ $$ LANGUAGE sql;
+ """
+ ),
+ # Rebuild the color tags so it's consisten in any project
+ migrations.RunSQL(
+ """
+ WITH
+ tags_colors AS (
+ SELECT id project_id, reduce_dim(tags_colors) tags_colors
+ FROM projects_project
+ WHERE tags_colors != '{}'
+ ),
+ tags AS (
+ SELECT unnest(tags) tag, NULL color, project_id FROM userstories_userstory
+ UNION
+ SELECT unnest(tags) tag, NULL color, project_id FROM tasks_task
+ UNION
+ SELECT unnest(tags) tag, NULL color, project_id FROM issues_issue
+ UNION
+ SELECT unnest(tags) tag, NULL color, id project_id FROM projects_project
+ ),
+ rebuilt_tags_colors AS (
+ SELECT tags.project_id project_id,
+ array_agg_mult(ARRAY[[tags.tag, tags_colors.tags_colors[2]]]) tags_colors
+ FROM tags
+ LEFT JOIN tags_colors ON
+ tags_colors.project_id = tags.project_id AND
+ tags_colors[1] = tags.tag
+ GROUP BY tags.project_id
+ )
+ UPDATE projects_project
+ SET tags_colors = rebuilt_tags_colors.tags_colors
+ FROM rebuilt_tags_colors
+ WHERE rebuilt_tags_colors.project_id = projects_project.id;
+ """
+ ),
+ # Trigger for auto updating projects_project.tags_colors
+ migrations.RunSQL(
+ """
+ CREATE OR REPLACE FUNCTION update_project_tags_colors()
+ RETURNS trigger AS $update_project_tags_colors$
+ DECLARE
+ tags text[];
+ project_tags_colors text[];
+ tag_color text[];
+ project_tags text[];
+ tag text;
+ project_id integer;
+ BEGIN
+ tags := NEW.tags::text[];
+ project_id := NEW.project_id::integer;
+ project_tags := '{}';
+
+ -- Read project tags_colors into project_tags_colors
+ SELECT projects_project.tags_colors INTO project_tags_colors
+ FROM projects_project
+ WHERE id = project_id;
+
+ -- Extract just the project tags to project_tags_colors
+ IF project_tags_colors != ARRAY[]::text[] THEN
+ FOREACH tag_color SLICE 1 in ARRAY project_tags_colors
+ LOOP
+ project_tags := array_append(project_tags, tag_color[1]);
+ END LOOP;
+ END IF;
+
+ -- Add to project_tags_colors the new tags
+ IF tags IS NOT NULL THEN
+ FOREACH tag in ARRAY tags
+ LOOP
+ IF tag != ALL(project_tags) THEN
+ project_tags_colors := array_cat(project_tags_colors,
+ ARRAY[ARRAY[tag, NULL]]);
+ END IF;
+ END LOOP;
+ END IF;
+
+ -- Save the result in the tags_colors column
+ UPDATE projects_project
+ SET tags_colors = project_tags_colors
+ WHERE id = project_id;
+
+ RETURN NULL;
+ END; $update_project_tags_colors$
+ LANGUAGE plpgsql;
+ """
+ ),
+
+ # Execute trigger after user_story update
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_userstory_update ON userstories_userstory;
+ CREATE TRIGGER update_project_tags_colors_on_userstory_update
+ AFTER UPDATE ON userstories_userstory
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ # Execute trigger after user_story insert
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_userstory_insert ON userstories_userstory;
+ CREATE TRIGGER update_project_tags_colors_on_userstory_insert
+ AFTER INSERT ON userstories_userstory
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ # Execute trigger after task update
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_task_update ON tasks_task;
+ CREATE TRIGGER update_project_tags_colors_on_task_update
+ AFTER UPDATE ON tasks_task
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ # Execute trigger after task insert
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_task_insert ON tasks_task;
+ CREATE TRIGGER update_project_tags_colors_on_task_insert
+ AFTER INSERT ON tasks_task
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ # Execute trigger after issue update
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_issue_update ON issues_issue;
+ CREATE TRIGGER update_project_tags_colors_on_issue_update
+ AFTER UPDATE ON issues_issue
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ # Execute trigger after issue insert
+ migrations.RunSQL(
+ """
+ DROP TRIGGER IF EXISTS update_project_tags_colors_on_issue_insert ON issues_issue;
+ CREATE TRIGGER update_project_tags_colors_on_issue_insert
+ AFTER INSERT ON issues_issue
+ FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors();
+ """
+ ),
+ ]
diff --git a/taiga/projects/migrations/0047_auto_20160614_1201.py b/taiga/projects/migrations/0047_auto_20160614_1201.py
new file mode 100644
index 00000000..eccd1f46
--- /dev/null
+++ b/taiga/projects/migrations/0047_auto_20160614_1201.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-14 12:01
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0046_triggers_to_update_tags_colors'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='anon_permissions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'),
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='public_permissions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'),
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='tags',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='tags_colors',
+ field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='tags colors'),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0048_auto_20160615_1508.py b/taiga/projects/migrations/0048_auto_20160615_1508.py
new file mode 100644
index 00000000..ab8ab0be
--- /dev/null
+++ b/taiga/projects/migrations/0048_auto_20160615_1508.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-15 15:08
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0047_auto_20160614_1201'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='projecttemplate',
+ options={'ordering': ['order', 'name'], 'verbose_name': 'project template', 'verbose_name_plural': 'project templates'},
+ ),
+ migrations.AddField(
+ model_name='projecttemplate',
+ name='order',
+ field=models.IntegerField(default=10000, verbose_name='user order'),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0049_auto_20160629_1443.py b/taiga/projects/migrations/0049_auto_20160629_1443.py
new file mode 100644
index 00000000..8c2117b0
--- /dev/null
+++ b/taiga/projects/migrations/0049_auto_20160629_1443.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-29 14:43
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+import django.db.models.deletion
+import django_pgjson.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0048_auto_20160615_1508'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='EpicStatus',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, verbose_name='name')),
+ ('slug', models.SlugField(blank=True, max_length=255, verbose_name='slug')),
+ ('order', models.IntegerField(default=10, verbose_name='order')),
+ ('is_closed', models.BooleanField(default=False, verbose_name='is closed')),
+ ('color', models.CharField(default='#999999', max_length=20, verbose_name='color')),
+ ],
+ options={
+ 'verbose_name_plural': 'epic statuses',
+ 'ordering': ['project', 'order', 'name'],
+ 'verbose_name': 'epic status',
+ },
+ ),
+ migrations.AlterModelOptions(
+ name='issuestatus',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'issue status', 'verbose_name_plural': 'issue statuses'},
+ ),
+ migrations.AlterModelOptions(
+ name='issuetype',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'issue type', 'verbose_name_plural': 'issue types'},
+ ),
+ migrations.AlterModelOptions(
+ name='membership',
+ options={'ordering': ['project', 'user__full_name', 'user__username', 'user__email', 'email'], 'verbose_name': 'membership', 'verbose_name_plural': 'memberships'},
+ ),
+ migrations.AlterModelOptions(
+ name='points',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'points', 'verbose_name_plural': 'points'},
+ ),
+ migrations.AlterModelOptions(
+ name='priority',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'priority', 'verbose_name_plural': 'priorities'},
+ ),
+ migrations.AlterModelOptions(
+ name='severity',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'severity', 'verbose_name_plural': 'severities'},
+ ),
+ migrations.AlterModelOptions(
+ name='taskstatus',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'task status', 'verbose_name_plural': 'task statuses'},
+ ),
+ migrations.AlterModelOptions(
+ name='userstorystatus',
+ options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'user story status', 'verbose_name_plural': 'user story statuses'},
+ ),
+ migrations.AddField(
+ model_name='project',
+ name='is_epics_activated',
+ field=models.BooleanField(default=False, verbose_name='active epics panel'),
+ ),
+ migrations.AddField(
+ model_name='projecttemplate',
+ name='epic_statuses',
+ field=django_pgjson.fields.JsonField(blank=True, null=True, verbose_name='epic statuses'),
+ ),
+ migrations.AddField(
+ model_name='projecttemplate',
+ name='is_epics_activated',
+ field=models.BooleanField(default=False, verbose_name='active epics panel'),
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='anon_permissions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_epics', 'View epic'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'),
+ ),
+ migrations.AlterField(
+ model_name='project',
+ name='public_permissions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epics', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'),
+ ),
+ migrations.AddField(
+ model_name='epicstatus',
+ name='project',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epic_statuses', to='projects.Project', verbose_name='project'),
+ ),
+ migrations.AddField(
+ model_name='project',
+ name='default_epic_status',
+ field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='projects.EpicStatus', verbose_name='default epic status'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='epicstatus',
+ unique_together=set([('project', 'slug'), ('project', 'name')]),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0050_project_epics_csv_uuid.py b/taiga/projects/migrations/0050_project_epics_csv_uuid.py
new file mode 100644
index 00000000..2dc87674
--- /dev/null
+++ b/taiga/projects/migrations/0050_project_epics_csv_uuid.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-20 17:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0049_auto_20160629_1443'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='project',
+ name='epics_csv_uuid',
+ field=models.CharField(blank=True, db_index=True, default=None, editable=False, max_length=32, null=True),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0051_auto_20160729_0802.py b/taiga/projects/migrations/0051_auto_20160729_0802.py
new file mode 100644
index 00000000..24767fdb
--- /dev/null
+++ b/taiga/projects/migrations/0051_auto_20160729_0802.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-29 08:02
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0050_project_epics_csv_uuid'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='project',
+ options={'ordering': ['name', 'id'], 'verbose_name': 'project', 'verbose_name_plural': 'projects'},
+ ),
+ ]
diff --git a/taiga/projects/migrations/0052_epic_status.py b/taiga/projects/migrations/0052_epic_status.py
new file mode 100644
index 00000000..baa9ab46
--- /dev/null
+++ b/taiga/projects/migrations/0052_epic_status.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-08-25 10:19
+from __future__ import unicode_literals
+
+from django.db import connection, migrations, models
+
+def update_epic_status(apps, schema_editor):
+ Project = apps.get_model("projects", "Project")
+ project_ids = Project.objects.filter(default_epic_status__isnull=True).values_list("id", flat=True)
+ if not project_ids:
+ return
+
+ values_sql = []
+ for project_id in project_ids:
+ values_sql.append("('New', 'new', 1, false, '#999999', {project_id})".format(project_id=project_id))
+ values_sql.append("('Ready', 'ready', 2, false, '#ff8a84', {project_id})".format(project_id=project_id))
+ values_sql.append("('In progress', 'in-progress', 3, false, '#ff9900', {project_id})".format(project_id=project_id))
+ values_sql.append("('Ready for test', 'ready-for-test', 4, false, '#fcc000', {project_id})".format(project_id=project_id))
+ values_sql.append("('Done', 'done', 5, true, '#669900', {project_id})".format(project_id=project_id))
+
+ sql = """
+ INSERT INTO projects_epicstatus (name, slug, "order", is_closed, color, project_id)
+ VALUES
+ {values};
+ """.format(values=','.join(values_sql))
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+
+def update_default_epic_status(apps, schema_editor):
+ sql = """
+ UPDATE projects_project
+ SET default_epic_status_id = projects_epicstatus.id
+ FROM projects_epicstatus
+ WHERE
+ projects_project.default_epic_status_id IS NULL
+ AND
+ projects_epicstatus.order = 1
+ AND
+ projects_epicstatus.project_id = projects_project.id;
+ """
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0051_auto_20160729_0802'),
+ ]
+
+ operations = [
+ migrations.RunPython(update_epic_status),
+ migrations.RunPython(update_default_epic_status)
+ ]
diff --git a/taiga/projects/migrations/0053_auto_20160927_0741.py b/taiga/projects/migrations/0053_auto_20160927_0741.py
new file mode 100644
index 00000000..0b4d3136
--- /dev/null
+++ b/taiga/projects/migrations/0053_auto_20160927_0741.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-27 07:41
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0052_epic_status'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='project',
+ name='creation_template',
+ field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='projects.ProjectTemplate', verbose_name='creation template'),
+ ),
+ ]
diff --git a/taiga/projects/migrations/0054_auto_20160928_0540.py b/taiga/projects/migrations/0054_auto_20160928_0540.py
new file mode 100644
index 00000000..6fe8def5
--- /dev/null
+++ b/taiga/projects/migrations/0054_auto_20160928_0540.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-28 05:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.time
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0053_auto_20160927_0741'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='membership',
+ name='user_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='user order'),
+ ),
+ migrations.AlterField(
+ model_name='projecttemplate',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='user order'),
+ ),
+ ]
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index f109060b..2e0047fc 100644
--- a/taiga/projects/milestones/api.py
+++ b/taiga/projects/milestones/api.py
@@ -17,24 +17,25 @@
# along with this program. If not, see .
from django.apps import apps
-from django.db.models import Prefetch
from taiga.base import filters
from taiga.base import response
from taiga.base.decorators import detail_route
-from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.db import get_object_or_none
-from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
+from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
-from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset
-from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watcher_to_queryset
from . import serializers
+from . import validators
from . import models
from . import permissions
+from . import utils as milestones_utils
import datetime
@@ -42,9 +43,14 @@ import datetime
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
BlockedByProjectMixin, ModelCrudViewSet):
serializer_class = serializers.MilestoneSerializer
+ validator_class = validators.MilestoneValidator
permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,)
- filter_fields = ("project", "closed")
+ filter_fields = (
+ "project",
+ "project__slug",
+ "closed"
+ )
queryset = models.Milestone.objects.all()
def list(self, request, *args, **kwargs):
@@ -69,32 +75,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
def get_queryset(self):
qs = super().get_queryset()
-
- # Userstories prefetching
- UserStory = apps.get_model("userstories", "UserStory")
- us_qs = UserStory.objects.prefetch_related("role_points",
- "role_points__points",
- "role_points__role")
-
- us_qs = us_qs.select_related("milestone",
- "project",
- "status",
- "owner",
- "assigned_to",
- "generated_from_issue")
-
- us_qs = self.attach_watchers_attrs_to_queryset(us_qs)
-
- if self.request.user.is_authenticated():
- us_qs = attach_is_voter_to_queryset(self.request.user, us_qs)
- us_qs = attach_is_watcher_to_queryset(us_qs, self.request.user)
-
- qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs))
-
- # Milestones prefetching
qs = qs.select_related("project", "owner")
- qs = self.attach_watchers_attrs_to_queryset(qs)
-
+ qs = milestones_utils.attach_extra_info(qs, user=self.request.user)
qs = qs.order_by("-estimated_start")
return qs
diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py
index 21d85b14..4488d178 100644
--- a/taiga/projects/milestones/models.py
+++ b/taiga/projects/milestones/models.py
@@ -16,9 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.apps import apps
from django.db import models
-from django.db.models import Prefetch, Count
+from django.db.models import Count
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
@@ -28,7 +27,7 @@ from django.utils.functional import cached_property
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.dicts import dict_sum
from taiga.projects.notifications.mixins import WatchedModelMixin
-from taiga.projects.userstories.models import UserStory
+from django_pglocks import advisory_lock
import itertools
import datetime
@@ -84,9 +83,11 @@ class Milestone(WatchedModelMixin, models.Model):
if not self._importing or not self.modified_date:
self.modified_date = timezone.now()
if not self.slug:
- self.slug = slugify_uniquely(self.name, self.__class__)
-
- super().save(*args, **kwargs)
+ with advisory_lock("milestone-creation-{}".format(self.project_id)):
+ self.slug = slugify_uniquely(self.name, self.__class__)
+ super().save(*args, **kwargs)
+ else:
+ super().save(*args, **kwargs)
@cached_property
def cached_user_stories(self):
diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py
index 2a52be47..44b3e8f4 100644
--- a/taiga/projects/milestones/serializers.py
+++ b/taiga/projects/milestones/serializers.py
@@ -16,28 +16,37 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.utils.translation import ugettext as _
-
from taiga.base.api import serializers
-from taiga.base.utils import json
-from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
-from ..userstories.serializers import UserStoryListSerializer
-from . import models
+from taiga.base.fields import Field, MethodField
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
+from taiga.projects.userstories.serializers import UserStoryListSerializer
-class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, ValidateDuplicatedNameInProjectMixin):
- user_stories = UserStoryListSerializer(many=True, required=False, read_only=True)
- total_points = serializers.SerializerMethodField("get_total_points")
- closed_points = serializers.SerializerMethodField("get_closed_points")
+class MilestoneSerializer(WatchedResourceSerializer, serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ owner = Field(attr="owner_id")
+ project = Field(attr="project_id")
+ estimated_start = Field()
+ estimated_finish = Field()
+ created_date = Field()
+ modified_date = Field()
+ closed = Field()
+ disponibility = Field()
+ order = Field()
+ watchers = Field()
+ user_stories = MethodField()
+ total_points = MethodField()
+ closed_points = MethodField()
- class Meta:
- model = models.Milestone
- read_only_fields = ("id", "created_date", "modified_date")
+ def get_user_stories(self, obj):
+ return UserStoryListSerializer(obj.user_stories.all(), many=True).data
def get_total_points(self, obj):
- return sum(obj.total_points.values())
+ assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
+ return obj.total_points_attr
def get_closed_points(self, obj):
- return sum(obj.closed_points.values())
+ assert hasattr(obj, "closed_points_attr"), "instance must have a closed_points_attr attribute"
+ return obj.closed_points_attr
diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py
new file mode 100644
index 00000000..bea1cf12
--- /dev/null
+++ b/taiga/projects/milestones/utils.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.apps import apps
+from django.db.models import Prefetch
+
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.userstories import utils as userstories_utils
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_total_points(queryset, as_field="total_points_attr"):
+ """Attach total of point values to each object of the queryset.
+
+ :param queryset: A Django milestones queryset object.
+ :param as_field: Attach the points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT SUM(projects_points.value)
+ FROM userstories_rolepoints
+ INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
+ INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
+ WHERE userstories_userstory.milestone_id = {tbl}.id"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_closed_points(queryset, as_field="closed_points_attr"):
+ """Attach total of closed point values to each object of the queryset.
+
+ :param queryset: A Django milestones queryset object.
+ :param as_field: Attach the points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT SUM(projects_points.value)
+ FROM userstories_rolepoints
+ INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
+ INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
+ WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None):
+ # Userstories prefetching
+ UserStory = apps.get_model("userstories", "UserStory")
+ us_queryset = UserStory.objects.select_related("milestone",
+ "project",
+ "status",
+ "owner",
+ "assigned_to",
+ "generated_from_issue")
+
+ us_queryset = userstories_utils.attach_total_points(us_queryset)
+ us_queryset = userstories_utils.attach_role_points(us_queryset)
+ us_queryset = userstories_utils.attach_epics(us_queryset)
+
+ us_queryset = attach_total_voters_to_queryset(us_queryset)
+ us_queryset = attach_watchers_to_queryset(us_queryset)
+ us_queryset = attach_total_watchers_to_queryset(us_queryset)
+ us_queryset = attach_is_voter_to_queryset(us_queryset, user)
+ us_queryset = attach_is_watcher_to_queryset(us_queryset, user)
+
+ queryset = queryset.prefetch_related(Prefetch("user_stories", queryset=us_queryset))
+
+ queryset = attach_total_points(queryset)
+ queryset = attach_closed_points(queryset)
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+
+ return queryset
diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py
index 3648a672..b7d4d484 100644
--- a/taiga/projects/milestones/validators.py
+++ b/taiga/projects/milestones/validators.py
@@ -18,15 +18,24 @@
from django.utils.translation import ugettext as _
-from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
+from taiga.base.api import validators
+from taiga.projects.validators import DuplicatedNameInProjectValidator
+from taiga.projects.notifications.validators import WatchersValidator
from . import models
-class SprintExistsValidator:
+class MilestoneExistsValidator:
def validate_sprint_id(self, attrs, source):
value = attrs[source]
if not models.Milestone.objects.filter(pk=value).exists():
- msg = _("There's no sprint with that id")
- raise serializers.ValidationError(msg)
+ msg = _("There's no milestone with that id")
+ raise ValidationError(msg)
return attrs
+
+
+class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Milestone
+ read_only_fields = ("id", "created_date", "modified_date")
diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py
index 07a9b683..c8a70932 100644
--- a/taiga/projects/mixins/serializers.py
+++ b/taiga/projects/mixins/serializers.py
@@ -16,26 +16,91 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api import serializers
-
from django.utils.translation import ugettext as _
-class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
+from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
+from taiga.projects import services
+from taiga.users.serializers import UserBasicInfoSerializer
- def validate_name(self, attrs, source):
- """
- Check the points name is not duplicated in the project on creation
- """
- model = self.opts.model
- qs = None
- # If the object exists:
- if self.object and attrs.get(source, None):
- qs = model.objects.filter(project=self.object.project, name=attrs[source]).exclude(id=self.object.id)
- if not self.object and attrs.get("project", None) and attrs.get(source, None):
- qs = model.objects.filter(project=attrs["project"], name=attrs[source])
+class CachedUsersSerializerMixin(serializers.LightSerializer):
+ def to_value(self, instance):
+ self._serialized_users = {}
+ return super().to_value(instance)
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
+ def get_user_extra_info(self, user):
+ if user is None:
+ return None
- return attrs
+ serialized_user = self._serialized_users.get(user.id, None)
+ if serialized_user is None:
+ serialized_user = UserBasicInfoSerializer(user).data
+ self._serialized_users[user.id] = serialized_user
+
+ return serialized_user
+
+
+class OwnerExtraInfoSerializerMixin(CachedUsersSerializerMixin):
+ owner = Field(attr="owner_id")
+ owner_extra_info = MethodField()
+
+ def get_owner_extra_info(self, obj):
+ return self.get_user_extra_info(obj.owner)
+
+
+class AssignedToExtraInfoSerializerMixin(CachedUsersSerializerMixin):
+ assigned_to = Field(attr="assigned_to_id")
+ assigned_to_extra_info = MethodField()
+
+ def get_assigned_to_extra_info(self, obj):
+ return self.get_user_extra_info(obj.assigned_to)
+
+
+class StatusExtraInfoSerializerMixin(serializers.LightSerializer):
+ status = Field(attr="status_id")
+ status_extra_info = MethodField()
+
+ def to_value(self, instance):
+ self._serialized_status = {}
+ return super().to_value(instance)
+
+ def get_status_extra_info(self, obj):
+ if obj.status_id is None:
+ return None
+
+ serialized_status = self._serialized_status.get(obj.status_id, None)
+ if serialized_status is None:
+ serialized_status = {
+ "name": _(obj.status.name),
+ "color": obj.status.color,
+ "is_closed": obj.status.is_closed
+ }
+ self._serialized_status[obj.status_id] = serialized_status
+
+ return serialized_status
+
+
+class ProjectExtraInfoSerializerMixin(serializers.LightSerializer):
+ project = Field(attr="project_id")
+ project_extra_info = MethodField()
+
+ def to_value(self, instance):
+ self._serialized_project = {}
+ return super().to_value(instance)
+
+ def get_project_extra_info(self, obj):
+ if obj.project_id is None:
+ return None
+
+ serialized_project = self._serialized_project.get(obj.project_id, None)
+ if serialized_project is None:
+ serialized_project = {
+ "name": obj.project.name,
+ "slug": obj.project.slug,
+ "logo_small_url": services.get_logo_small_thumbnail_url(obj.project),
+ "id": obj.project_id
+ }
+ self._serialized_project[obj.project_id] = serialized_project
+
+ return serialized_project
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 4467baa8..f6f6cc52 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -16,32 +16,30 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import itertools
-import uuid
-
from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.db import models
-from django.db.models import signals, Q
+from django.db.models import Q
from django.apps import apps
-from django.conf import settings
-from django.dispatch import receiver
-from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.utils.functional import cached_property
-from django_pgjson.fields import JsonField
-from djorm_pgarray.fields import TextArrayField
+from django_pglocks import advisory_lock
-from taiga.base.tags import TaggedMixin
-from taiga.base.utils.dicts import dict_sum
+from django_pgjson.fields import JsonField
+
+from taiga.base.utils.time import timestamp_ms
+from taiga.projects.tagging.models import TaggedMixin
+from taiga.projects.tagging.models import TagsColorsdMixin
from taiga.base.utils.files import get_file_path
from taiga.base.utils.sequence import arithmetic_progression
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.slug import slugify_uniquely_for_queryset
-from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
+from taiga.permissions.choices import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.services import (
@@ -87,9 +85,15 @@ class Membership(models.Model):
invitation_extra_text = models.TextField(null=True, blank=True,
verbose_name=_("invitation extra text"))
- user_order = models.IntegerField(default=10000, null=False, blank=False,
+ user_order = models.BigIntegerField(default=timestamp_ms, null=False, blank=False,
verbose_name=_("user order"))
+ class Meta:
+ verbose_name = "membership"
+ verbose_name_plural = "memberships"
+ unique_together = ("user", "project",)
+ ordering = ["project", "user__full_name", "user__username", "user__email", "email"]
+
def get_related_people(self):
related_people = get_user_model().objects.filter(id=self.user.id)
return related_people
@@ -100,24 +104,19 @@ class Membership(models.Model):
if self.user and memberships.count() > 0 and memberships[0].id != self.id:
raise ValidationError(_('The user is already member of the project'))
- class Meta:
- verbose_name = "membership"
- verbose_name_plural = "membershipss"
- unique_together = ("user", "project",)
- ordering = ["project", "user__full_name", "user__username", "user__email", "email"]
- permissions = (
- ("view_membership", "Can view membership"),
- )
-
class ProjectDefaults(models.Model):
- default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL,
- related_name="+", null=True, blank=True,
- verbose_name=_("default points"))
+ default_epic_status = models.OneToOneField("projects.EpicStatus",
+ on_delete=models.SET_NULL, related_name="+",
+ null=True, blank=True,
+ verbose_name=_("default epic status"))
default_us_status = models.OneToOneField("projects.UserStoryStatus",
on_delete=models.SET_NULL, related_name="+",
null=True, blank=True,
verbose_name=_("default US status"))
+ default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL,
+ related_name="+", null=True, blank=True,
+ verbose_name=_("default points"))
default_task_status = models.OneToOneField("projects.TaskStatus",
on_delete=models.SET_NULL, related_name="+",
null=True, blank=True,
@@ -141,7 +140,7 @@ class ProjectDefaults(models.Model):
abstract = True
-class Project(ProjectDefaults, TaggedMixin, models.Model):
+class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model):
name = models.CharField(max_length=250, null=False, blank=False,
verbose_name=_("name"))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
@@ -167,6 +166,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
verbose_name=_("total of milestones"))
total_story_points = models.FloatField(null=True, blank=True, verbose_name=_("total story points"))
+ is_epics_activated = models.BooleanField(default=False, null=False, blank=True,
+ verbose_name=_("active epics panel"))
is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active backlog panel"))
is_kanban_activated = models.BooleanField(default=False, null=False, blank=True,
@@ -183,19 +184,16 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
creation_template = models.ForeignKey("projects.ProjectTemplate",
related_name="projects", null=True,
+ on_delete=models.SET_NULL,
blank=True, default=None,
verbose_name=_("creation template"))
- anon_permissions = TextArrayField(blank=True, null=True,
- default=[],
- verbose_name=_("anonymous permissions"),
- choices=ANON_PERMISSIONS)
- public_permissions = TextArrayField(blank=True, null=True,
- default=[],
- verbose_name=_("user permissions"),
- choices=MEMBERS_PERMISSIONS)
is_private = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("is private"))
+ anon_permissions = ArrayField(models.TextField(null=False, blank=False, choices=ANON_PERMISSIONS),
+ null=True, blank=True, default=[], verbose_name=_("anonymous permissions"))
+ public_permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS),
+ null=True, blank=True, default=[], verbose_name=_("user permissions"))
is_featured = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is featured"))
@@ -205,6 +203,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
looking_for_people_note = models.TextField(default="", null=False, blank=True,
verbose_name=_("loking for people note"))
+ epics_csv_uuid = models.CharField(max_length=32, editable=False, null=True,
+ blank=True, default=None, db_index=True)
userstories_csv_uuid = models.CharField(max_length=32, editable=False,
null=True, blank=True,
default=None, db_index=True)
@@ -214,9 +214,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
null=True, blank=True, default=None,
db_index=True)
- tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
- verbose_name=_("tags colors"))
-
transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
verbose_name=_("project transfer token"))
@@ -262,10 +259,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
["name", "id"],
]
- permissions = (
- ("view_project", "Can view project"),
- )
-
def __str__(self):
return self.name
@@ -276,16 +269,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
if not self._importing or not self.modified_date:
self.modified_date = timezone.now()
- if not self.slug:
- base_name = "{}-{}".format(self.owner.username, self.name)
- base_slug = slugify_uniquely(base_name, self.__class__)
- slug = base_slug
- for i in arithmetic_progression():
- if not type(self).objects.filter(slug=slug).exists() or i > 100:
- break
- slug = "{}-{}".format(base_slug, i)
- self.slug = slug
-
if not self.is_backlog_activated:
self.total_milestones = None
self.total_story_points = None
@@ -296,13 +279,25 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
if not self.is_looking_for_people:
self.looking_for_people_note = ""
- if self.anon_permissions == None:
+ if self.anon_permissions is None:
self.anon_permissions = []
- if self.public_permissions == None:
+ if self.public_permissions is None:
self.public_permissions = []
- super().save(*args, **kwargs)
+ if not self.slug:
+ with advisory_lock("project-creation"):
+ base_name = "{}-{}".format(self.owner.username, self.name)
+ base_slug = slugify_uniquely(base_name, self.__class__)
+ slug = base_slug
+ for i in arithmetic_progression():
+ if not type(self).objects.filter(slug=slug).exists() or i > 100:
+ break
+ slug = "{}-{}".format(base_slug, i)
+ self.slug = slug
+ super().save(*args, **kwargs)
+ else:
+ super().save(*args, **kwargs)
def refresh_totals(self, save=True):
now = timezone.now()
@@ -377,7 +372,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
@cached_property
def cached_memberships(self):
- return {m.user.id: m for m in self.memberships.exclude(user__isnull=True).select_related("user", "project", "role")}
+ return {m.user.id: m for m in self.memberships.exclude(user__isnull=True)
+ .select_related("user", "project", "role")}
def cached_memberships_for_user(self, user):
return self.cached_memberships.get(user.id, None)
@@ -510,6 +506,39 @@ class ProjectModulesConfig(models.Model):
ordering = ["project"]
+# Epic common Models
+class EpicStatus(models.Model):
+ name = models.CharField(max_length=255, null=False, blank=False,
+ verbose_name=_("name"))
+ slug = models.SlugField(max_length=255, null=False, blank=True,
+ verbose_name=_("slug"))
+ order = models.IntegerField(default=10, null=False, blank=False,
+ verbose_name=_("order"))
+ is_closed = models.BooleanField(default=False, null=False, blank=True,
+ verbose_name=_("is closed"))
+ color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
+ verbose_name=_("color"))
+ project = models.ForeignKey("Project", null=False, blank=False,
+ related_name="epic_statuses", verbose_name=_("project"))
+
+ class Meta:
+ verbose_name = "epic status"
+ verbose_name_plural = "epic statuses"
+ ordering = ["project", "order", "name"]
+ unique_together = (("project", "name"), ("project", "slug"))
+
+ def __str__(self):
+ return self.name
+
+ def save(self, *args, **kwargs):
+ qs = self.project.epic_statuses
+ if self.id:
+ qs = qs.exclude(id=self.id)
+
+ self.slug = slugify_uniquely_for_queryset(self.name, qs)
+ return super().save(*args, **kwargs)
+
+
# User Stories common Models
class UserStoryStatus(models.Model):
name = models.CharField(max_length=255, null=False, blank=False,
@@ -534,9 +563,6 @@ class UserStoryStatus(models.Model):
verbose_name_plural = "user story statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
- permissions = (
- ("view_userstorystatus", "Can view user story status"),
- )
def __str__(self):
return self.name
@@ -565,9 +591,6 @@ class Points(models.Model):
verbose_name_plural = "points"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
- permissions = (
- ("view_points", "Can view points"),
- )
def __str__(self):
return self.name
@@ -594,9 +617,6 @@ class TaskStatus(models.Model):
verbose_name_plural = "task statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
- permissions = (
- ("view_taskstatus", "Can view task status"),
- )
def __str__(self):
return self.name
@@ -627,9 +647,6 @@ class Priority(models.Model):
verbose_name_plural = "priorities"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
- permissions = (
- ("view_priority", "Can view priority"),
- )
def __str__(self):
return self.name
@@ -650,9 +667,6 @@ class Severity(models.Model):
verbose_name_plural = "severities"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
- permissions = (
- ("view_severity", "Can view severity"),
- )
def __str__(self):
return self.name
@@ -677,9 +691,6 @@ class IssueStatus(models.Model):
verbose_name_plural = "issue statuses"
ordering = ["project", "order", "name"]
unique_together = (("project", "name"), ("project", "slug"))
- permissions = (
- ("view_issuestatus", "Can view issue status"),
- )
def __str__(self):
return self.name
@@ -708,9 +719,6 @@ class IssueType(models.Model):
verbose_name_plural = "issue types"
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
- permissions = (
- ("view_issuetype", "Can view issue type"),
- )
def __str__(self):
return self.name
@@ -723,6 +731,8 @@ class ProjectTemplate(models.Model):
verbose_name=_("slug"), unique=True)
description = models.TextField(null=False, blank=False,
verbose_name=_("description"))
+ order = models.BigIntegerField(default=timestamp_ms, null=False, blank=False,
+ verbose_name=_("user order"))
created_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("created date"),
default=timezone.now)
@@ -732,6 +742,8 @@ class ProjectTemplate(models.Model):
blank=False,
verbose_name=_("default owner's role"))
+ is_epics_activated = models.BooleanField(default=False, null=False, blank=True,
+ verbose_name=_("active epics panel"))
is_backlog_activated = models.BooleanField(default=True, null=False, blank=True,
verbose_name=_("active backlog panel"))
is_kanban_activated = models.BooleanField(default=False, null=False, blank=True,
@@ -747,6 +759,7 @@ class ProjectTemplate(models.Model):
verbose_name=_("videoconference extra data"))
default_options = JsonField(null=True, blank=True, verbose_name=_("default options"))
+ epic_statuses = JsonField(null=True, blank=True, verbose_name=_("epic statuses"))
us_statuses = JsonField(null=True, blank=True, verbose_name=_("us statuses"))
points = JsonField(null=True, blank=True, verbose_name=_("points"))
task_statuses = JsonField(null=True, blank=True, verbose_name=_("task statuses"))
@@ -760,7 +773,7 @@ class ProjectTemplate(models.Model):
class Meta:
verbose_name = "project template"
verbose_name_plural = "project templates"
- ordering = ["name"]
+ ordering = ["order", "name"]
def __str__(self):
return self.name
@@ -774,6 +787,7 @@ class ProjectTemplate(models.Model):
super().save(*args, **kwargs)
def load_data_from_project(self, project):
+ self.is_epics_activated = project.is_epics_activated
self.is_backlog_activated = project.is_backlog_activated
self.is_kanban_activated = project.is_kanban_activated
self.is_wiki_activated = project.is_wiki_activated
@@ -783,6 +797,7 @@ class ProjectTemplate(models.Model):
self.default_options = {
"points": getattr(project.default_points, "name", None),
+ "epic_status": getattr(project.default_epic_status, "name", None),
"us_status": getattr(project.default_us_status, "name", None),
"task_status": getattr(project.default_task_status, "name", None),
"issue_status": getattr(project.default_issue_status, "name", None),
@@ -791,6 +806,16 @@ class ProjectTemplate(models.Model):
"severity": getattr(project.default_severity, "name", None)
}
+ self.epic_statuses = []
+ for epic_status in project.epic_statuses.all():
+ self.epic_statuses.append({
+ "name": epic_status.name,
+ "slug": epic_status.slug,
+ "is_closed": epic_status.is_closed,
+ "color": epic_status.color,
+ "order": epic_status.order,
+ })
+
self.us_statuses = []
for us_status in project.us_statuses.all():
self.us_statuses.append({
@@ -878,6 +903,7 @@ class ProjectTemplate(models.Model):
raise Exception("Project need an id (must be a saved project)")
project.creation_template = self
+ project.is_epics_activated = self.is_epics_activated
project.is_backlog_activated = self.is_backlog_activated
project.is_kanban_activated = self.is_kanban_activated
project.is_wiki_activated = self.is_wiki_activated
@@ -885,6 +911,16 @@ class ProjectTemplate(models.Model):
project.videoconferences = self.videoconferences
project.videoconferences_extra_data = self.videoconferences_extra_data
+ for epic_status in self.epic_statuses:
+ EpicStatus.objects.create(
+ name=epic_status["name"],
+ slug=epic_status["slug"],
+ is_closed=epic_status["is_closed"],
+ color=epic_status["color"],
+ order=epic_status["order"],
+ project=project
+ )
+
for us_status in self.us_statuses:
UserStoryStatus.objects.create(
name=us_status["name"],
@@ -959,12 +995,16 @@ class ProjectTemplate(models.Model):
permissions=role['permissions']
)
- if self.points:
- project.default_points = Points.objects.get(name=self.default_options["points"],
- project=project)
+ if self.epic_statuses:
+ project.default_epic_status = EpicStatus.objects.get(name=self.default_options["epic_status"],
+ project=project)
+
if self.us_statuses:
project.default_us_status = UserStoryStatus.objects.get(name=self.default_options["us_status"],
project=project)
+ if self.points:
+ project.default_points = Points.objects.get(name=self.default_options["points"],
+ project=project)
if self.task_statuses:
project.default_task_status = TaskStatus.objects.get(name=self.default_options["task_status"],
@@ -978,9 +1018,11 @@ class ProjectTemplate(models.Model):
project=project)
if self.priorities:
- project.default_priority = Priority.objects.get(name=self.default_options["priority"], project=project)
+ project.default_priority = Priority.objects.get(name=self.default_options["priority"],
+ project=project)
if self.severities:
- project.default_severity = Severity.objects.get(name=self.default_options["severity"], project=project)
+ project.default_severity = Severity.objects.get(name=self.default_options["severity"],
+ project=project)
return project
diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py
index cd8f564e..9936ae52 100644
--- a/taiga/projects/notifications/api.py
+++ b/taiga/projects/notifications/api.py
@@ -21,9 +21,7 @@ from django.db.models import Q
from taiga.base.api import ModelCrudViewSet
from taiga.projects.notifications.choices import NotifyLevel
-from taiga.projects.notifications.models import Watched
from taiga.projects.models import Project
-from taiga.users import services as user_services
from . import serializers
from . import models
from . import permissions
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index ee1d59f8..e9dff950 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -15,6 +15,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+
from functools import partial
from operator import is_not
@@ -25,16 +26,12 @@ from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import serializers
from taiga.base.api.utils import get_object_or_404
-from taiga.base.fields import WatchersField
+from taiga.base.fields import WatchersField, MethodField
from taiga.projects.notifications import services
-from taiga.projects.notifications.utils import (attach_watchers_to_queryset,
- attach_is_watcher_to_queryset,
- attach_total_watchers_to_queryset)
from . serializers import WatcherSerializer
-
class WatchedResourceMixin:
"""
Rest Framework resource mixin for resources susceptible
@@ -51,14 +48,6 @@ class WatchedResourceMixin:
_not_notify = False
- def attach_watchers_attrs_to_queryset(self, queryset):
- queryset = attach_watchers_to_queryset(queryset)
- queryset = attach_total_watchers_to_queryset(queryset)
- if self.request.user.is_authenticated():
- queryset = attach_is_watcher_to_queryset(queryset, self.request.user)
-
- return queryset
-
@detail_route(methods=["POST"])
def watch(self, request, pk=None):
obj = self.get_object()
@@ -183,14 +172,15 @@ class WatchedModelMixin(object):
return frozenset(filter(is_not_none, participants))
-class WatchedResourceModelSerializer(serializers.ModelSerializer):
- is_watcher = serializers.SerializerMethodField("get_is_watcher")
- total_watchers = serializers.SerializerMethodField("get_total_watchers")
+class WatchedResourceSerializer(serializers.LightSerializer):
+ is_watcher = MethodField()
+ total_watchers = MethodField()
def get_is_watcher(self, obj):
+ # The "is_watcher" attribute is attached in the get_queryset of the viewset.
if "request" in self.context:
user = self.context["request"].user
- return user.is_authenticated() and user.is_watcher(obj)
+ return user.is_authenticated() and getattr(obj, "is_watcher", False)
return False
@@ -199,18 +189,18 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
return getattr(obj, "total_watchers", 0) or 0
-class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
+class EditableWatchedResourceSerializer(serializers.ModelSerializer):
watchers = WatchersField(required=False)
def restore_object(self, attrs, instance=None):
- #watchers is not a field from the model but can be attached in the get_queryset of the viewset.
- #If that's the case we need to remove it before calling the super method
- watcher_field = self.fields.pop("watchers", None)
+ # watchers is not a field from the model but can be attached in the get_queryset of the viewset.
+ # If that's the case we need to remove it before calling the super method
+ self.fields.pop("watchers", None)
self.validate_watchers(attrs, "watchers")
new_watcher_ids = attrs.pop("watchers", None)
- obj = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
+ obj = super(EditableWatchedResourceSerializer, self).restore_object(attrs, instance)
- #A partial update can exclude the watchers field or if the new instance can still not be saved
+ # A partial update can exclude the watchers field or if the new instance can still not be saved
if instance is None or new_watcher_ids is None:
return obj
@@ -219,7 +209,6 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
- User = get_user_model()
adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids)
removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids)
for user in adding_users:
@@ -233,7 +222,7 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
return obj
def to_native(self, obj):
- #if watchers wasn't attached via the get_queryset of the viewset we need to manually add it
+ # if watchers wasn't attached via the get_queryset of the viewset we need to manually add it
if obj is not None:
if not hasattr(obj, "watchers"):
obj.watchers = [user.id for user in obj.get_watchers()]
@@ -243,10 +232,10 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
if user and user.is_authenticated():
obj.is_watcher = user.id in obj.watchers
- return super(WatchedResourceModelSerializer, self).to_native(obj)
+ return super(WatchedResourceSerializer, self).to_native(obj)
def save(self, **kwargs):
- obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs)
+ obj = super(EditableWatchedResourceSerializer, self).save(**kwargs)
self.fields["watchers"] = WatchersField(required=False)
obj.watchers = [user.id for user in obj.get_watchers()]
return obj
diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py
index 8d88c6c6..e998d068 100644
--- a/taiga/projects/notifications/services.py
+++ b/taiga/projects/notifications/services.py
@@ -36,7 +36,7 @@ from taiga.projects.history.choices import HistoryType
from taiga.projects.history.services import (make_key_from_model_object,
get_last_snapshot_for_key,
get_model_from_key)
-from taiga.permissions.service import user_has_perm
+from taiga.permissions.services import user_has_perm
from .models import HistoryChangeNotification, Watched
@@ -112,6 +112,7 @@ def _filter_by_permissions(obj, user):
UserStory = apps.get_model("userstories", "UserStory")
Issue = apps.get_model("issues", "Issue")
Task = apps.get_model("tasks", "Task")
+ Epic = apps.get_model("epics", "Epic")
WikiPage = apps.get_model("wiki", "WikiPage")
if isinstance(obj, UserStory):
@@ -120,6 +121,8 @@ def _filter_by_permissions(obj, user):
return user_has_perm(user, "view_issues", obj, cache="project")
elif isinstance(obj, Task):
return user_has_perm(user, "view_tasks", obj, cache="project")
+ elif isinstance(obj, Epic):
+ return user_has_perm(user, "view_epics", obj, cache="project")
elif isinstance(obj, WikiPage):
return user_has_perm(user, "view_wiki_pages", obj, cache="project")
return False
@@ -223,6 +226,7 @@ def send_notifications(obj, *, history):
if settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL == 0:
send_sync_notifications(notification.id)
+
@transaction.atomic
def send_sync_notifications(notification_id):
"""
@@ -261,19 +265,21 @@ def send_sync_notifications(notification_id):
msg_id = 'taiga-system'
now = datetime.datetime.now()
- format_args = {"project_slug": notification.project.slug,
- "project_name": notification.project.name,
- "msg_id": msg_id,
- "time": int(now.timestamp()),
- "domain": domain}
+ format_args = {
+ "project_slug": notification.project.slug,
+ "project_name": notification.project.name,
+ "msg_id": msg_id,
+ "time": int(now.timestamp()),
+ "domain": domain
+ }
- headers = {"Message-ID": "<{project_slug}/{msg_id}/{time}@{domain}>".format(**format_args),
- "In-Reply-To": "<{project_slug}/{msg_id}@{domain}>".format(**format_args),
- "References": "<{project_slug}/{msg_id}@{domain}>".format(**format_args),
-
- "List-ID": 'Taiga/{project_name} '.format(**format_args),
-
- "Thread-Index": make_ms_thread_index("<{project_slug}/{msg_id}@{domain}>".format(**format_args), now)}
+ headers = {
+ "Message-ID": "<{project_slug}/{msg_id}/{time}@{domain}>".format(**format_args),
+ "In-Reply-To": "<{project_slug}/{msg_id}@{domain}>".format(**format_args),
+ "References": "<{project_slug}/{msg_id}@{domain}>".format(**format_args),
+ "List-ID": 'Taiga/{project_name} '.format(**format_args),
+ "Thread-Index": make_ms_thread_index("<{project_slug}/{msg_id}@{domain}>".format(**format_args), now)
+ }
for user in notification.notify_users.distinct():
context["user"] = user
@@ -330,6 +336,7 @@ def get_related_people(obj):
related_people = related_people.exclude(is_active=False)
related_people = related_people.exclude(is_system=True)
related_people = related_people.distinct()
+
return related_people
@@ -370,9 +377,11 @@ def get_projects_watched(user_or_id):
user = get_user_model().objects.get(id=user_or_id)
project_class = apps.get_model("projects", "Project")
- project_ids = user.notify_policies.exclude(notify_level=NotifyLevel.none).values_list("project__id", flat=True)
+ project_ids = (user.notify_policies.exclude(notify_level=NotifyLevel.none)
+ .values_list("project__id", flat=True))
return project_class.objects.filter(id__in=project_ids)
+
def add_watcher(obj, user):
"""Add a watcher to an object.
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja b/taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja
new file mode 100644
index 00000000..5c84885d
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja
@@ -0,0 +1,10 @@
+{% extends "emails/updates-body-html.jinja" %}
+
+{% block head %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %}
+ Epic updated
+ Hello {{ user }},
{{ changer }} has updated a epic on {{ project }}
+ Epic #{{ ref }} {{ subject }}
+ See epic
+ {% endtrans %}
+{% endblock %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja b/taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja
new file mode 100644
index 00000000..1d6800e2
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja
@@ -0,0 +1,8 @@
+{% extends "emails/updates-body-text.jinja" %}
+{% block head %}
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %}
+Epic updated
+Hello {{ user }}, {{ changer }} has updated a epic on {{ project }}
+See epic #{{ ref }} {{ subject }} at {{ url }}
+{% endtrans %}
+{% endblock %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja b/taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja
new file mode 100644
index 00000000..d66464e0
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+[{{ project }}] Updated the epic #{{ ref }} "{{ subject }}"
+{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja b/taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja
new file mode 100644
index 00000000..0484ee0b
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja
@@ -0,0 +1,11 @@
+{% extends "emails/base-body-html.jinja" %}
+
+{% block body %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %}
+ New epic created
+ Hello {{ user }},
{{ changer }} has created a new epic on {{ project }}
+ Epic #{{ ref }} {{ subject }}
+ See epic
+ The Taiga Team
+ {% endtrans %}
+{% endblock %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja b/taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja
new file mode 100644
index 00000000..51748107
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja
@@ -0,0 +1,8 @@
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %}
+New epic created
+Hello {{ user }}, {{ changer }} has created a new epic on {{ project }}
+See epic #{{ ref }} {{ subject }} at {{ url }}
+
+---
+The Taiga Team
+{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja b/taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja
new file mode 100644
index 00000000..d41e9c78
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+[{{ project }}] Created the epic #{{ ref }} "{{ subject }}"
+{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja
new file mode 100644
index 00000000..0debb545
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja
@@ -0,0 +1,11 @@
+{% extends "emails/base-body-html.jinja" %}
+
+{% block body %}
+ {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
+ Epic deleted
+ Hello {{ user }},
{{ changer }} has deleted a epic on {{ project }}
+ Epic #{{ ref }} {{ subject }}
+ The Taiga Team
+ {% endtrans %}
+{% endblock %}
+
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja
new file mode 100644
index 00000000..b5855eba
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja
@@ -0,0 +1,8 @@
+{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}
+Epic deleted
+Hello {{ user }}, {{ changer }} has deleted a epic on {{ project }}
+Epic #{{ ref }} {{ subject }}
+
+---
+The Taiga Team
+{% endtrans %}
diff --git a/taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja b/taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja
new file mode 100644
index 00000000..65286ec2
--- /dev/null
+++ b/taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja
@@ -0,0 +1,3 @@
+{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %}
+[{{ project }}] Deleted the epic #{{ ref }} "{{ subject }}"
+{% endtrans %}
diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py
index 00b98d63..ae6bd34c 100644
--- a/taiga/projects/notifications/utils.py
+++ b/taiga/projects/notifications/utils.py
@@ -53,15 +53,18 @@ def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"):
"""
model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
- sql = ("""SELECT CASE WHEN (SELECT count(*)
- FROM notifications_watched
- WHERE notifications_watched.content_type_id = {type_id}
- AND notifications_watched.object_id = {tbl}.id
- AND notifications_watched.user_id = {user_id}) > 0
- THEN TRUE
- ELSE FALSE
- END""")
- sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
+ if user is None or user.is_anonymous():
+ sql = """SELECT false"""
+ else:
+ sql = ("""SELECT CASE WHEN (SELECT count(*)
+ FROM notifications_watched
+ WHERE notifications_watched.content_type_id = {type_id}
+ AND notifications_watched.object_id = {tbl}.id
+ AND notifications_watched.user_id = {user_id}) > 0
+ THEN TRUE
+ ELSE FALSE
+ END""")
+ sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
qs = queryset.extra(select={as_field: sql})
return qs
diff --git a/taiga/projects/notifications/validators.py b/taiga/projects/notifications/validators.py
index 851cc309..40e02083 100644
--- a/taiga/projects/notifications/validators.py
+++ b/taiga/projects/notifications/validators.py
@@ -18,7 +18,7 @@
from django.utils.translation import ugettext as _
-from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
class WatchersValidator:
@@ -45,6 +45,6 @@ class WatchersValidator:
existing_watcher_ids = project.get_watchers().values_list("id", flat=True)
result = set(users).difference(member_ids).difference(existing_watcher_ids)
if result:
- raise serializers.ValidationError(_("Watchers contains invalid users"))
+ raise ValidationError(_("Watchers contains invalid users"))
return attrs
diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py
index b76b674d..7c10b5c2 100644
--- a/taiga/projects/permissions.py
+++ b/taiga/projects/permissions.py
@@ -19,18 +19,21 @@
from django.utils.translation import ugettext as _
from taiga.base.api.permissions import TaigaResourcePermission
-from taiga.base.api.permissions import HasProjectPerm
from taiga.base.api.permissions import IsAuthenticated
-from taiga.base.api.permissions import IsProjectAdmin
from taiga.base.api.permissions import AllowAny
from taiga.base.api.permissions import IsSuperUser
+from taiga.base.api.permissions import IsObjectOwner
from taiga.base.api.permissions import PermissionComponent
from taiga.base import exceptions as exc
-from taiga.projects.models import Membership
+from taiga.permissions.permissions import HasProjectPerm
+from taiga.permissions.permissions import IsProjectAdmin
+
+from . import models
from . import services
+
class CanLeaveProject(PermissionComponent):
def check_permissions(self, request, view, obj=None):
if not obj or not request.user.is_authenticated():
@@ -38,20 +41,12 @@ class CanLeaveProject(PermissionComponent):
try:
if not services.can_user_leave_project(request.user, obj):
- raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are no more admins"))
+ raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are "
+ "no more admins"))
return True
- except Membership.DoesNotExist:
+ except models.Membership.DoesNotExist:
return False
-class IsMainOwner(PermissionComponent):
- def check_permissions(self, request, view, obj=None):
- if not obj or not request.user.is_authenticated():
- return False
-
- if obj.owner is None:
- return False
-
- return obj.owner == request.user
class ProjectPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
@@ -67,6 +62,7 @@ class ProjectPermission(TaigaResourcePermission):
stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project')
issues_stats_perms = HasProjectPerm('view_project')
+ regenerate_epics_csv_uuid_perms = IsProjectAdmin()
regenerate_userstories_csv_uuid_perms = IsProjectAdmin()
regenerate_issues_csv_uuid_perms = IsProjectAdmin()
regenerate_tasks_csv_uuid_perms = IsProjectAdmin()
@@ -80,9 +76,13 @@ class ProjectPermission(TaigaResourcePermission):
leave_perms = CanLeaveProject()
transfer_validate_token_perms = IsAuthenticated() & HasProjectPerm('view_project')
transfer_request_perms = IsProjectAdmin()
- transfer_start_perms = IsMainOwner()
+ transfer_start_perms = IsObjectOwner()
transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project')
transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project')
+ create_tag_perms = IsProjectAdmin()
+ edit_tag_perms = IsProjectAdmin()
+ delete_tag_perms = IsProjectAdmin()
+ mix_tags_perms = IsProjectAdmin()
class ProjectFansPermission(TaigaResourcePermission):
@@ -110,6 +110,18 @@ class MembershipPermission(TaigaResourcePermission):
resend_invitation_perms = IsProjectAdmin()
+# Epics
+
+class EpicStatusPermission(TaigaResourcePermission):
+ retrieve_perms = HasProjectPerm('view_project')
+ create_perms = IsProjectAdmin()
+ update_perms = IsProjectAdmin()
+ partial_update_perms = IsProjectAdmin()
+ destroy_perms = IsProjectAdmin()
+ list_perms = AllowAny()
+ bulk_update_order_perms = IsProjectAdmin()
+
+
# User Stories
class PointsPermission(TaigaResourcePermission):
diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py
index 013aa11c..a4ae20ec 100644
--- a/taiga/projects/references/api.py
+++ b/taiga/projects/references/api.py
@@ -22,9 +22,9 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base.api import viewsets
from taiga.base.api.utils import get_object_or_404
-from taiga.permissions.service import user_has_perm
+from taiga.permissions.services import user_has_perm
-from .serializers import ResolverSerializer
+from .validators import ResolverValidator
from . import permissions
@@ -32,11 +32,11 @@ class ResolverViewSet(viewsets.ViewSet):
permission_classes = (permissions.ResolverPermission,)
def list(self, request, **kwargs):
- serializer = ResolverSerializer(data=request.QUERY_PARAMS)
- if not serializer.is_valid():
- raise exc.BadRequest(serializer.errors)
+ validator = ResolverValidator(data=request.QUERY_PARAMS)
+ if not validator.is_valid():
+ raise exc.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project_model = apps.get_model("projects", "Project")
project = get_object_or_404(project_model, slug=data["project"])
@@ -45,6 +45,9 @@ class ResolverViewSet(viewsets.ViewSet):
result = {"project": project.pk}
+ if data["epic"] and user_has_perm(request.user, "view_epics", project):
+ result["epic"] = get_object_or_404(project.epics.all(),
+ ref=data["epic"]).pk
if data["us"] and user_has_perm(request.user, "view_us", project):
result["us"] = get_object_or_404(project.user_stories.all(),
ref=data["us"]).pk
@@ -63,6 +66,11 @@ class ResolverViewSet(viewsets.ViewSet):
if data["ref"]:
ref_found = False # No need to continue once one ref is found
+ if ref_found is False and user_has_perm(request.user, "view_epics", project):
+ epic = project.epics.filter(ref=data["ref"]).first()
+ if epic:
+ result["epic"] = epic.pk
+ ref_found = True
if user_has_perm(request.user, "view_us", project):
us = project.user_stories.filter(ref=data["ref"]).first()
if us:
diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py
index 40aea018..61097ecb 100644
--- a/taiga/projects/references/models.py
+++ b/taiga/projects/references/models.py
@@ -21,10 +21,11 @@ from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
+from taiga.projects.models import Project
+from taiga.projects.epics.models import Epic
from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task
from taiga.projects.issues.models import Issue
-from taiga.projects.models import Project
from . import sequences as seq
@@ -103,11 +104,22 @@ def attach_sequence(sender, instance, created, **kwargs):
instance.save(update_fields=['ref'])
+# Project
models.signals.post_save.connect(create_sequence, sender=Project, dispatch_uid="refproj")
-models.signals.pre_save.connect(store_previous_project, sender=UserStory, dispatch_uid="refus")
-models.signals.pre_save.connect(store_previous_project, sender=Issue, dispatch_uid="refissue")
-models.signals.pre_save.connect(store_previous_project, sender=Task, dispatch_uid="reftask")
-models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid="refus")
-models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue")
-models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask")
models.signals.post_delete.connect(delete_sequence, sender=Project, dispatch_uid="refprojdel")
+
+# Epic
+models.signals.pre_save.connect(store_previous_project, sender=Epic, dispatch_uid="refepic")
+models.signals.post_save.connect(attach_sequence, sender=Epic, dispatch_uid="refepic")
+
+# User Story
+models.signals.pre_save.connect(store_previous_project, sender=UserStory, dispatch_uid="refus")
+models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid="refus")
+
+# Task
+models.signals.pre_save.connect(store_previous_project, sender=Task, dispatch_uid="reftask")
+models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask")
+
+# Issue
+models.signals.pre_save.connect(store_previous_project, sender=Issue, dispatch_uid="refissue")
+models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue")
diff --git a/taiga/projects/references/serializers.py b/taiga/projects/references/validators.py
similarity index 71%
rename from taiga/projects/references/serializers.py
rename to taiga/projects/references/validators.py
index fb9ad177..e91adb21 100644
--- a/taiga/projects/references/serializers.py
+++ b/taiga/projects/references/validators.py
@@ -17,11 +17,14 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
-class ResolverSerializer(serializers.Serializer):
+class ResolverValidator(validators.Validator):
project = serializers.CharField(max_length=512, required=True)
milestone = serializers.CharField(max_length=512, required=False)
+ epic = serializers.IntegerField(required=False)
us = serializers.IntegerField(required=False)
task = serializers.IntegerField(required=False)
issue = serializers.IntegerField(required=False)
@@ -30,11 +33,13 @@ class ResolverSerializer(serializers.Serializer):
def validate(self, attrs):
if "ref" in attrs:
+ if "epic" in attrs:
+ raise ValidationError("'epic' param is incompatible with 'ref' in the same request")
if "us" in attrs:
- raise serializers.ValidationError("'us' param is incompatible with 'ref' in the same request")
+ raise ValidationError("'us' param is incompatible with 'ref' in the same request")
if "task" in attrs:
- raise serializers.ValidationError("'task' param is incompatible with 'ref' in the same request")
+ raise ValidationError("'task' param is incompatible with 'ref' in the same request")
if "issue" in attrs:
- raise serializers.ValidationError("'issue' param is incompatible with 'ref' in the same request")
+ raise ValidationError("'issue' param is incompatible with 'ref' in the same request")
return attrs
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 15867ce1..eb7b2e54 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -16,131 +16,165 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
from django.utils.translation import ugettext as _
-from django.db.models import Q
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
-from taiga.base.fields import PgArrayField
-from taiga.base.fields import TagsField
-from taiga.base.fields import TagsColorsField
+from taiga.base.fields import Field, MethodField, I18NField
-from taiga.projects.notifications.choices import NotifyLevel
-from taiga.users.services import get_photo_or_gravatar_url
-from taiga.users.serializers import UserSerializer
+from taiga.permissions import services as permissions_services
+from taiga.users.services import get_photo_url, get_user_photo_url
+from taiga.users.gravatar import get_gravatar_id, get_user_gravatar_id
from taiga.users.serializers import UserBasicInfoSerializer
-from taiga.users.serializers import ProjectRoleSerializer
-from taiga.users.validators import RoleExistsValidator
-from taiga.permissions.service import get_user_project_permissions
-from taiga.permissions.service import is_project_admin, is_project_owner
-from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
+from taiga.permissions.services import calculate_permissions
+from taiga.permissions.services import is_project_admin, is_project_owner
-from . import models
from . import services
-from .notifications.mixins import WatchedResourceModelSerializer
-from .validators import ProjectExistsValidator
-from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
-from .custom_attributes.serializers import TaskCustomAttributeSerializer
-from .custom_attributes.serializers import IssueCustomAttributeSerializer
-from .likes.mixins.serializers import FanResourceSerializerMixin
+from .notifications.choices import NotifyLevel
######################################################
-## Custom values for selectors
+# Custom values for selectors
######################################################
-class PointsSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Points
- i18n_fields = ("name",)
+class EpicStatusSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.UserStoryStatus
- i18n_fields = ("name",)
+class UserStoryStatusSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ is_archived = Field()
+ color = Field()
+ wip_limit = Field()
+ project = Field(attr="project_id")
-class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.UserStoryStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
+class PointsSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ order = Field()
+ value = Field()
+ project = Field(attr="project_id")
-class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.TaskStatus
- i18n_fields = ("name",)
+class TaskStatusSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.TaskStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
+class SeveritySerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class SeveritySerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Severity
- i18n_fields = ("name",)
+class PrioritySerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class PrioritySerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Priority
- i18n_fields = ("name",)
+class IssueStatusSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class IssueStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.IssueStatus
- i18n_fields = ("name",)
-
-
-class BasicIssueStatusSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.IssueStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
-
-
-class IssueTypeSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.IssueType
- i18n_fields = ("name",)
+class IssueTypeSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
######################################################
-## Members
+# Members
######################################################
-class MembershipSerializer(serializers.ModelSerializer):
- role_name = serializers.CharField(source='role.name', required=False, read_only=True, i18n=True)
- full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True)
- user_email = serializers.EmailField(source='user.email', required=False, read_only=True)
- is_user_active = serializers.BooleanField(source='user.is_active', required=False,
- read_only=True)
- email = serializers.EmailField(required=True)
- color = serializers.CharField(source='user.color', required=False, read_only=True)
- photo = serializers.SerializerMethodField("get_photo")
- project_name = serializers.SerializerMethodField("get_project_name")
- project_slug = serializers.SerializerMethodField("get_project_slug")
- invited_by = UserBasicInfoSerializer(read_only=True)
- is_owner = serializers.SerializerMethodField("get_is_owner")
+class MembershipDictSerializer(serializers.LightDictSerializer):
+ role = Field()
+ role_name = Field()
+ full_name = Field()
+ full_name_display = MethodField()
+ is_active = Field()
+ id = Field()
+ color = Field()
+ username = Field()
+ photo = MethodField()
+ gravatar_id = MethodField()
- class Meta:
- model = models.Membership
- # IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
- # with this info (excluding here user_email and email)
- read_only_fields = ("user",)
- exclude = ("token", "user_email", "email")
+ def get_full_name_display(self, obj):
+ return obj["full_name"] or obj["username"] or obj["email"]
- def get_photo(self, project):
- return get_photo_or_gravatar_url(project.user)
+ def get_photo(self, obj):
+ return get_photo_url(obj['photo'])
+
+ def get_gravatar_id(self, obj):
+ return get_gravatar_id(obj['email'])
+
+
+class MembershipSerializer(serializers.LightSerializer):
+ id = Field()
+ user = Field(attr="user_id")
+ project = Field(attr="project_id")
+ role = Field(attr="role_id")
+ is_admin = Field()
+ created_at = Field()
+ invited_by = Field(attr="invited_by_id")
+ invitation_extra_text = Field()
+ user_order = Field()
+ role_name = MethodField()
+ full_name = MethodField()
+ is_user_active = MethodField()
+ color = MethodField()
+ photo = MethodField()
+ gravatar_id = MethodField()
+ project_name = MethodField()
+ project_slug = MethodField()
+ invited_by = UserBasicInfoSerializer()
+ is_owner = MethodField()
+
+ def get_role_name(self, obj):
+ return obj.role.name if obj.role else None
+
+ def get_full_name(self, obj):
+ return obj.user.get_full_name() if obj.user else None
+
+ def get_is_user_active(self, obj):
+ return obj.user.is_active if obj.user else False
+
+ def get_color(self, obj):
+ return obj.user.color if obj.user else None
+
+ def get_photo(self, obj):
+ return get_user_photo_url(obj.user)
+
+ def get_gravatar_id(self, obj):
+ return get_user_gravatar_id(obj.user)
def get_project_name(self, obj):
return obj.project.name if obj and obj.project else ""
@@ -152,131 +186,125 @@ class MembershipSerializer(serializers.ModelSerializer):
return (obj and obj.user_id and obj.project_id and obj.project.owner_id and
obj.user_id == obj.project.owner_id)
- def validate_email(self, attrs, source):
- project = attrs.get("project", None)
- if project is None:
- project = self.object.project
-
- email = attrs[source]
-
- qs = models.Membership.objects.all()
-
- # If self.object is not None, the serializer is in update
- # mode, and for it, it should exclude self.
- if self.object:
- qs = qs.exclude(pk=self.object.pk)
-
- qs = qs.filter(Q(project_id=project.id, user__email=email) |
- Q(project_id=project.id, email=email))
-
- if qs.count() > 0:
- raise serializers.ValidationError(_("Email address is already taken"))
-
- return attrs
-
- def validate_role(self, attrs, source):
- project = attrs.get("project", None)
- if project is None:
- project = self.object.project
-
- role = attrs[source]
-
- if project.roles.filter(id=role.id).count() == 0:
- raise serializers.ValidationError(_("Invalid role for the project"))
-
- return attrs
-
- def validate_is_admin(self, attrs, source):
- project = attrs.get("project", None)
- if project is None:
- project = self.object.project
-
- if (self.object and self.object.user):
- if self.object.user.id == project.owner_id and attrs[source] != True:
- raise serializers.ValidationError(_("The project owner must be admin."))
-
- if not services.project_has_valid_admins(project, exclude_user=self.object.user):
- raise serializers.ValidationError(_("At least one user must be an active admin for this project."))
-
- return attrs
-
class MembershipAdminSerializer(MembershipSerializer):
- class Meta:
- model = models.Membership
- # IMPORTANT: Maintain the MembershipSerializer Meta up to date
- # with this info (excluding there user_email and email)
- read_only_fields = ("user",)
- exclude = ("token",)
+ email = Field()
+ user_email = MethodField()
+ def get_user_email(self, obj):
+ return obj.user.email if obj.user else None
-class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer):
- email = serializers.EmailField()
- role_id = serializers.IntegerField()
-
-
-class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_memberships = MemberBulkSerializer(many=True)
- invitation_extra_text = serializers.CharField(required=False, max_length=255)
-
-
-class ProjectMemberSerializer(serializers.ModelSerializer):
- id = serializers.IntegerField(source="user.id", read_only=True)
- username = serializers.CharField(source='user.username', read_only=True)
- full_name = serializers.CharField(source='user.full_name', read_only=True)
- full_name_display = serializers.CharField(source='user.get_full_name', read_only=True)
- color = serializers.CharField(source='user.color', read_only=True)
- photo = serializers.SerializerMethodField("get_photo")
- is_active = serializers.BooleanField(source='user.is_active', read_only=True)
- role_name = serializers.CharField(source='role.name', read_only=True, i18n=True)
-
- class Meta:
- model = models.Membership
- exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text",
- "user_order")
-
- def get_photo(self, membership):
- return get_photo_or_gravatar_url(membership.user)
+ # IMPORTANT: Maintain the MembershipSerializer Meta up to date
+ # with this info (excluding there user_email and email)
######################################################
-## Projects
+# Projects
######################################################
-class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer,
- serializers.ModelSerializer):
- anon_permissions = PgArrayField(required=False)
- public_permissions = PgArrayField(required=False)
- my_permissions = serializers.SerializerMethodField("get_my_permissions")
+class ProjectSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ description = Field()
+ created_date = Field()
+ modified_date = Field()
+ owner = MethodField()
+ members = MethodField()
+ total_milestones = Field()
+ total_story_points = Field()
+ is_epics_activated = Field()
+ is_backlog_activated = Field()
+ is_kanban_activated = Field()
+ is_wiki_activated = Field()
+ is_issues_activated = Field()
+ videoconferences = Field()
+ videoconferences_extra_data = Field()
+ creation_template = Field(attr="creation_template_id")
+ is_private = Field()
+ anon_permissions = Field()
+ public_permissions = Field()
+ is_featured = Field()
+ is_looking_for_people = Field()
+ looking_for_people_note = Field()
+ blocked_code = Field()
+ totals_updated_datetime = Field()
+ total_fans = Field()
+ total_fans_last_week = Field()
+ total_fans_last_month = Field()
+ total_fans_last_year = Field()
+ total_activity = Field()
+ total_activity_last_week = Field()
+ total_activity_last_month = Field()
+ total_activity_last_year = Field()
- owner = UserBasicInfoSerializer(read_only=True)
- i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
- i_am_admin = serializers.SerializerMethodField("get_i_am_admin")
- i_am_member = serializers.SerializerMethodField("get_i_am_member")
+ tags = Field()
+ tags_colors = MethodField()
- tags = TagsField(default=[], required=False)
- tags_colors = TagsColorsField(required=False)
+ default_epic_status = Field(attr="default_epic_status_id")
+ default_points = Field(attr="default_points_id")
+ default_us_status = Field(attr="default_us_status_id")
+ default_task_status = Field(attr="default_task_status_id")
+ default_priority = Field(attr="default_priority_id")
+ default_severity = Field(attr="default_severity_id")
+ default_issue_status = Field(attr="default_issue_status_id")
+ default_issue_type = Field(attr="default_issue_type_id")
- notify_level = serializers.SerializerMethodField("get_notify_level")
- total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
- total_watchers = serializers.SerializerMethodField("get_total_watchers")
+ my_permissions = MethodField()
- logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
- logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
+ i_am_owner = MethodField()
+ i_am_admin = MethodField()
+ i_am_member = MethodField()
- class Meta:
- model = models.Project
- read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
- exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
- "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid",
- "transfer_token")
+ notify_level = MethodField()
+ total_closed_milestones = MethodField()
+
+ is_watcher = MethodField()
+ total_watchers = MethodField()
+
+ logo_small_url = MethodField()
+ logo_big_url = MethodField()
+
+ is_fan = Field(attr="is_fan_attr")
+
+ def get_members(self, obj):
+ assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
+ if obj.members_attr is None:
+ return []
+
+ return [m.get("id") for m in obj.members_attr if m["id"] is not None]
+
+ def get_i_am_member(self, obj):
+ assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
+ if obj.members_attr is None:
+ return False
+
+ if "request" in self.context:
+ user = self.context["request"].user
+ user_ids = [m.get("id") for m in obj.members_attr if m["id"] is not None]
+ if not user.is_anonymous() and user.id in user_ids:
+ return True
+
+ return False
+
+ def get_tags_colors(self, obj):
+ return dict(obj.tags_colors)
def get_my_permissions(self, obj):
if "request" in self.context:
- return get_user_project_permissions(self.context["request"].user, obj)
+ user = self.context["request"].user
+ return calculate_permissions(is_authenticated=user.is_authenticated(),
+ is_superuser=user.is_superuser,
+ is_member=self.get_i_am_member(obj),
+ is_admin=self.get_i_am_admin(obj),
+ role_permissions=obj.my_role_permissions_attr,
+ anon_permissions=obj.anon_permissions,
+ public_permissions=obj.public_permissions)
return []
+ def get_owner(self, obj):
+ return UserBasicInfoSerializer(obj.owner).data
+
def get_i_am_owner(self, obj):
if "request" in self.context:
return is_project_owner(self.context["request"].user, obj)
@@ -287,35 +315,35 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return is_project_admin(self.context["request"].user, obj)
return False
- def get_i_am_member(self, obj):
- if "request" in self.context:
- user = self.context["request"].user
- if not user.is_anonymous() and user.cached_membership_for_project(obj):
- return True
- return False
-
def get_total_closed_milestones(self, obj):
- # The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
- qs_closed_milestones = getattr(obj, "closed_milestones", None)
- if qs_closed_milestones is not None:
- return len(qs_closed_milestones)
+ assert hasattr(obj, "closed_milestones_attr"), "instance must have a closed_milestones_attr attribute"
+ return obj.closed_milestones_attr
- return obj.milestones.filter(closed=True).count()
-
- def get_notify_level(self, obj):
- if "request" in self.context:
- user = self.context["request"].user
- return user.is_authenticated() and user.get_notify_level(obj)
-
- return None
+ def get_is_watcher(self, obj):
+ assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
+ np = self.get_notify_level(obj)
+ return np is not None and np != NotifyLevel.none
def get_total_watchers(self, obj):
- # The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset.
- qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None)
- if qs_valid_notify_policies is not None:
- return len(qs_valid_notify_policies)
+ assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
+ if obj.notify_policies_attr is None:
+ return 0
- return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
+ valid_notify_policies = [np for np in obj.notify_policies_attr if np["notify_level"] != NotifyLevel.none]
+ return len(valid_notify_policies)
+
+ def get_notify_level(self, obj):
+ assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
+ if obj.notify_policies_attr is None:
+ return None
+
+ if "request" in self.context:
+ user = self.context["request"].user
+ for np in obj.notify_policies_attr:
+ if np["user_id"] == user.id:
+ return np["notify_level"]
+
+ return None
def get_logo_small_url(self, obj):
return services.get_logo_small_thumbnail_url(obj)
@@ -325,94 +353,141 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
class ProjectDetailSerializer(ProjectSerializer):
- us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories
- points = PointsSerializer(many=True, required=False)
+ epic_statuses = Field(attr="epic_statuses_attr")
+ us_statuses = Field(attr="userstory_statuses_attr")
+ points = Field(attr="points_attr")
+ task_statuses = Field(attr="task_statuses_attr")
+ issue_statuses = Field(attr="issue_statuses_attr")
+ issue_types = Field(attr="issue_types_attr")
+ priorities = Field(attr="priorities_attr")
+ severities = Field(attr="severities_attr")
+ epic_custom_attributes = Field(attr="epic_custom_attributes_attr")
+ userstory_custom_attributes = Field(attr="userstory_custom_attributes_attr")
+ task_custom_attributes = Field(attr="task_custom_attributes_attr")
+ issue_custom_attributes = Field(attr="issue_custom_attributes_attr")
+ roles = Field(attr="roles_attr")
+ members = MethodField()
+ total_memberships = MethodField()
+ is_out_of_owner_limits = MethodField()
- task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks
+ # Admin fields
+ is_private_extra_info = MethodField()
+ max_memberships = MethodField()
+ epics_csv_uuid = Field()
+ userstories_csv_uuid = Field()
+ tasks_csv_uuid = Field()
+ issues_csv_uuid = Field()
+ transfer_token = Field()
+ milestones = MethodField()
- issue_statuses = IssueStatusSerializer(many=True, required=False)
- issue_types = IssueTypeSerializer(many=True, required=False)
- priorities = PrioritySerializer(many=True, required=False) # Issues
- severities = SeveritySerializer(many=True, required=False)
+ def get_milestones(self, obj):
+ assert hasattr(obj, "milestones_attr"), "instance must have a milestones_attr attribute"
+ if obj.milestones_attr is None:
+ return []
- userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes",
- many=True, required=False)
- task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes",
- many=True, required=False)
- issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
- many=True, required=False)
+ return obj.milestones_attr
- roles = ProjectRoleSerializer(source="roles", many=True, read_only=True)
- members = serializers.SerializerMethodField(method_name="get_members")
- total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
- is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits")
+ def to_value(self, instance):
+ # Name attributes must be translated
+ for attr in ["epic_statuses_attr", "userstory_statuses_attr", "points_attr", "task_statuses_attr",
+ "issue_statuses_attr", "issue_types_attr", "priorities_attr", "severities_attr",
+ "epic_custom_attributes_attr", "userstory_custom_attributes_attr",
+ "task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]:
+
+ assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
+ val = getattr(instance, attr)
+ if val is None:
+ continue
+
+ for elem in val:
+ elem["name"] = _(elem["name"])
+
+ ret = super().to_value(instance)
+
+ admin_fields = [
+ "epics_csv_uuid", "userstories_csv_uuid", "tasks_csv_uuid", "issues_csv_uuid",
+ "is_private_extra_info", "max_memberships", "transfer_token",
+ ]
+
+ is_admin_user = False
+ if "request" in self.context:
+ user = self.context["request"].user
+ is_admin_user = permissions_services.is_project_admin(user, instance)
+
+ if not is_admin_user:
+ for admin_field in admin_fields:
+ del(ret[admin_field])
+
+ return ret
def get_members(self, obj):
- qs = obj.memberships.filter(user__isnull=False)
- qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"})
- qs = qs.order_by("complete_user_name")
- qs = qs.select_related("role", "user")
- serializer = ProjectMemberSerializer(qs, many=True)
- return serializer.data
+ assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
+ if obj.members_attr is None:
+ return []
+
+ return MembershipDictSerializer([m for m in obj.members_attr if m['id'] is not None], many=True).data
def get_total_memberships(self, obj):
- return services.get_total_project_memberships(obj)
+ if obj.members_attr is None:
+ return 0
+
+ return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj):
- return services.check_if_project_is_out_of_owner_limits(obj)
-
-
-class ProjectDetailAdminSerializer(ProjectDetailSerializer):
- is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info")
- max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
-
- class Meta:
- model = models.Project
- read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
- exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
+ assert hasattr(obj, "private_projects_same_owner_attr"), ("instance must have a private_projects_same"
+ "_owner_attr attribute")
+ assert hasattr(obj, "public_projects_same_owner_attr"), ("instance must have a public_projects_same_"
+ "owner_attr attribute")
+ return services.check_if_project_is_out_of_owner_limits(
+ obj,
+ current_memberships=self.get_total_memberships(obj),
+ current_private_projects=obj.private_projects_same_owner_attr,
+ current_public_projects=obj.public_projects_same_owner_attr
+ )
def get_is_private_extra_info(self, obj):
- return services.check_if_project_privacity_can_be_changed(obj)
+ assert hasattr(obj, "private_projects_same_owner_attr"), ("instance must have a private_projects_same_"
+ "owner_attr attribute")
+ assert hasattr(obj, "public_projects_same_owner_attr"), ("instance must have a public_projects_same"
+ "_owner_attr attribute")
+ return services.check_if_project_privacity_can_be_changed(
+ obj,
+ current_memberships=self.get_total_memberships(obj),
+ current_private_projects=obj.private_projects_same_owner_attr,
+ current_public_projects=obj.public_projects_same_owner_attr
+ )
def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj)
######################################################
-## Liked
+# Project Templates
######################################################
-class LikedSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Project
- fields = ['id', 'name', 'slug']
-
-
-
-######################################################
-## Project Templates
-######################################################
-
-class ProjectTemplateSerializer(serializers.ModelSerializer):
- default_options = JsonField(required=False, label=_("Default options"))
- us_statuses = JsonField(required=False, label=_("User story's statuses"))
- points = JsonField(required=False, label=_("Points"))
- task_statuses = JsonField(required=False, label=_("Task's statuses"))
- issue_statuses = JsonField(required=False, label=_("Issue's statuses"))
- issue_types = JsonField(required=False, label=_("Issue's types"))
- priorities = JsonField(required=False, label=_("Priorities"))
- severities = JsonField(required=False, label=_("Severities"))
- roles = JsonField(required=False, label=_("Roles"))
-
- class Meta:
- model = models.ProjectTemplate
- read_only_fields = ("created_date", "modified_date")
- i18n_fields = ("name", "description")
-
-######################################################
-## Project order bulk serializers
-######################################################
-
-class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- order = serializers.IntegerField()
+class ProjectTemplateSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ description = I18NField()
+ order = Field()
+ created_date = Field()
+ modified_date = Field()
+ default_owner_role = Field()
+ is_epics_activated = Field()
+ is_backlog_activated = Field()
+ is_kanban_activated = Field()
+ is_wiki_activated = Field()
+ is_issues_activated = Field()
+ videoconferences = Field()
+ videoconferences_extra_data = Field()
+ default_options = Field()
+ epic_statuses = Field()
+ us_statuses = Field()
+ points = Field()
+ task_statuses = Field()
+ issue_statuses = Field()
+ issue_types = Field()
+ priorities = Field()
+ severities = Field()
+ roles = Field()
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index fb3cb9c5..3be0a9d8 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -19,7 +19,7 @@
# This makes all code that import services works and
# is not the baddest practice ;)
-from .bulk_update_order import update_projects_order_in_bulk
+from .bulk_update_order import apply_order_updates
from .bulk_update_order import bulk_update_severity_order
from .bulk_update_order import bulk_update_priority_order
from .bulk_update_order import bulk_update_issue_type_order
@@ -27,6 +27,8 @@ from .bulk_update_order import bulk_update_issue_status_order
from .bulk_update_order import bulk_update_task_status_order
from .bulk_update_order import bulk_update_points_order
from .bulk_update_order import bulk_update_userstory_status_order
+from .bulk_update_order import bulk_update_epic_status_order
+from .bulk_update_order import update_projects_order_in_bulk
from .filters import get_all_tags
@@ -55,7 +57,5 @@ from .stats import get_stats_for_project_issues
from .stats import get_stats_for_project
from .stats import get_member_stats_for_project
-from .tags_colors import update_project_tags_colors_handler
-
from .transfer import request_project_transfer, start_project_transfer
from .transfer import accept_project_transfer, reject_project_transfer
diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py
index 48e85218..614fd507 100644
--- a/taiga/projects/services/bulk_update_order.py
+++ b/taiga/projects/services/bulk_update_order.py
@@ -24,25 +24,83 @@ from taiga.projects import models
from contextlib import suppress
-def update_projects_order_in_bulk(bulk_data:list, field:str, user):
+def apply_order_updates(base_orders: dict, new_orders: dict):
+ """
+ `base_orders` must be a dict containing all the elements that can be affected by
+ order modifications.
+ `new_orders` must be a dict containing the basic order modifications to apply.
+
+ The result will a base_orders with the specified order changes in new_orders
+ and the extra calculated ones applied.
+ Extra order updates can be needed when moving elements to intermediate positions.
+ The elements where no order update is needed will be removed.
+ """
+ updated_order_ids = set()
+ # We will apply the multiple order changes by the new position order
+ sorted_new_orders = [(k, v) for k, v in new_orders.items()]
+ sorted_new_orders = sorted(sorted_new_orders, key=lambda e: e[1])
+
+ for new_order in sorted_new_orders:
+ old_order = base_orders[new_order[0]]
+ new_order = new_order[1]
+ for id, order in base_orders.items():
+ # When moving forward only the elements contained in the range new_order - old_order
+ # positions need to be updated
+ moving_backward = new_order <= old_order and order >= new_order and order < old_order
+ # When moving backward all the elements from the new_order position need to bee updated
+ moving_forward = new_order >= old_order and order >= new_order
+ if moving_backward or moving_forward:
+ base_orders[id] += 1
+ updated_order_ids.add(id)
+
+ # Overwritting the orders specified
+ for id, order in new_orders.items():
+ if base_orders[id] != order:
+ base_orders[id] = order
+ updated_order_ids.add(id)
+
+ # Remove not modified elements
+ removing_keys = [id for id in base_orders if id not in updated_order_ids]
+ [base_orders.pop(id, None) for id in removing_keys]
+
+
+def update_projects_order_in_bulk(bulk_data: list, field: str, user):
"""
Update the order of user projects in the user membership.
- `bulk_data` should be a list of tuples with the following format:
+ `bulk_data` should be a list of dicts with the following format:
- [(, {: , ...}), ...]
+ [{'project_id': , 'order': }, ...]
"""
- membership_ids = []
- new_order_values = []
+ memberships_orders = {m.id: getattr(m, field) for m in user.memberships.all()}
+ new_memberships_orders = {}
+
for membership_data in bulk_data:
project_id = membership_data["project_id"]
with suppress(ObjectDoesNotExist):
membership = user.memberships.get(project_id=project_id)
- membership_ids.append(membership.id)
- new_order_values.append({field: membership_data["order"]})
+ new_memberships_orders[membership.id] = membership_data["order"]
+
+ apply_order_updates(memberships_orders, new_memberships_orders)
from taiga.base.utils import db
+ db.update_attr_in_bulk_for_ids(memberships_orders, field, model=models.Membership)
- db.update_in_bulk_with_ids(membership_ids, new_order_values, model=models.Membership)
+
+@transaction.atomic
+def bulk_update_epic_status_order(project, user, data):
+ cursor = connection.cursor()
+
+ sql = """
+ prepare bulk_update_order as update projects_epicstatus set "order" = $1
+ where projects_epicstatus.id = $2 and
+ projects_epicstatus.project_id = $3;
+ """
+ cursor.execute(sql)
+ for id, order in data:
+ cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);",
+ (order, id, project.id))
+ cursor.execute("DEALLOCATE bulk_update_order")
+ cursor.close()
@transaction.atomic
diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py
index f56a9941..2bd31d94 100644
--- a/taiga/projects/services/projects.py
+++ b/taiga/projects/services/projects.py
@@ -27,30 +27,45 @@ ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects'
ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner'
-def check_if_project_privacity_can_be_changed(project):
+def check_if_project_privacity_can_be_changed(project,
+ current_memberships=None,
+ current_private_projects=None,
+ current_public_projects=None):
"""Return if the project privacity can be changed from private to public or viceversa.
:param project: A project object.
+ :param current_memberships: Project total memberships, If None it will be calculated.
+ :param current_private_projects: total private projects owned by the project owner, If None it will be calculated.
+ :param current_public_projects: total public projects owned by the project owner, If None it will be calculated.
:return: A dict like this {'can_be_updated': bool, 'reason': error message}.
"""
if project.owner is None:
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
- if project.is_private:
+ if current_memberships is None:
current_memberships = project.memberships.count()
+
+ if project.is_private:
max_memberships = project.owner.max_memberships_public_projects
error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
- current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ if current_public_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ else:
+ current_projects = current_public_projects
+
max_projects = project.owner.max_public_projects
error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
else:
- current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_private_projects
error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
- current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ if current_private_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ else:
+ current_projects = current_private_projects
+
max_projects = project.owner.max_private_projects
error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
@@ -139,25 +154,43 @@ def check_if_project_can_be_transfered(project, new_owner):
return (True, None)
-def check_if_project_is_out_of_owner_limits(project):
+def check_if_project_is_out_of_owner_limits(project,
+ current_memberships=None,
+ current_private_projects=None,
+ current_public_projects=None):
+
"""Return if the project fits on its owner limits.
:param project: A project object.
+ :param current_memberships: Project total memberships, If None it will be calculated.
+ :param current_private_projects: total private projects owned by the project owner, If None it will be calculated.
+ :param current_public_projects: total public projects owned by the project owner, If None it will be calculated.
:return: bool
"""
if project.owner is None:
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
- if project.is_private:
+ if current_memberships is None:
current_memberships = project.memberships.count()
+
+ if project.is_private:
max_memberships = project.owner.max_memberships_private_projects
- current_projects = project.owner.owned_projects.filter(is_private=True).count()
+
+ if current_private_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ else:
+ current_projects = current_private_projects
+
max_projects = project.owner.max_private_projects
else:
- current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_public_projects
- current_projects = project.owner.owned_projects.filter(is_private=False).count()
+
+ if current_public_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ else:
+ current_projects = current_public_projects
+
max_projects = project.owner.max_public_projects
if max_memberships is not None and current_memberships > max_memberships:
diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py
deleted file mode 100644
index 9b9aa962..00000000
--- a/taiga/projects/services/tags_colors.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2014-2016 Andrey Antukh
-# Copyright (C) 2014-2016 Jesús Espino
-# Copyright (C) 2014-2016 David Barragán
-# Copyright (C) 2014-2016 Alejandro Alonso
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-
-from django.conf import settings
-
-from taiga.projects.services.filters import get_all_tags
-from taiga.projects.models import Project
-
-from hashlib import sha1
-
-
-def _generate_color(tag):
- color = sha1(tag.encode("utf-8")).hexdigest()[0:6]
- return "#{}".format(color)
-
-
-def _get_new_color(tag, predefined_colors, exclude=[]):
- colors = list(set(predefined_colors) - set(exclude))
- if colors:
- return colors[0]
- return _generate_color(tag)
-
-
-def remove_unused_tags(project):
- current_tags = get_all_tags(project)
- project.tags_colors = list(filter(lambda x: x[0] in current_tags, project.tags_colors))
-
-
-def update_project_tags_colors_handler(instance):
- if instance.tags is None:
- instance.tags = []
-
- if not isinstance(instance.project.tags_colors, list):
- instance.project.tags_colors = []
-
- for tag in instance.tags:
- defined_tags = map(lambda x: x[0], instance.project.tags_colors)
- if tag not in defined_tags:
- used_colors = map(lambda x: x[1], instance.project.tags_colors)
- new_color = _get_new_color(tag, settings.TAGS_PREDEFINED_COLORS,
- exclude=used_colors)
- instance.project.tags_colors.append([tag, new_color])
-
- remove_unused_tags(instance.project)
-
- if not isinstance(instance, Project):
- instance.project.save()
diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py
index 7db244da..b94e5cda 100644
--- a/taiga/projects/signals.py
+++ b/taiga/projects/signals.py
@@ -19,7 +19,6 @@
from django.apps import apps
from django.conf import settings
-from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags
from taiga.projects.notifications.services import create_notify_policy_if_not_exists
from taiga.base.utils.db import get_typename_for_model_class
@@ -30,20 +29,7 @@ from easy_thumbnails.files import get_thumbnailer
# Signals over project items
####################################
-## TAGS
-
-def tags_normalization(sender, instance, **kwargs):
- if isinstance(instance.tags, (list, tuple)):
- instance.tags = list(map(str.lower, instance.tags))
-
-
-def update_project_tags_when_create_or_edit_taggable_item(sender, instance, **kwargs):
- update_project_tags_colors_handler(instance)
-
-
-def update_project_tags_when_delete_taggable_item(sender, instance, **kwargs):
- remove_unused_tags(instance.project)
- instance.project.save()
+## Membership
def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points()
@@ -68,7 +54,6 @@ def project_post_save(sender, instance, created, **kwargs):
if instance._importing:
return
-
template = getattr(instance, "creation_template", None)
if template is None:
ProjectTemplate = apps.get_model("projects", "ProjectTemplate")
diff --git a/taiga/projects/tagging/__init__.py b/taiga/projects/tagging/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py
new file mode 100644
index 00000000..db57b946
--- /dev/null
+++ b/taiga/projects/tagging/api.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base import response
+from taiga.base.decorators import detail_route
+from taiga.base.utils.collections import OrderedSet
+
+from . import services
+from . import validators
+
+
+class TagsColorsResourceMixin:
+ @detail_route(methods=["GET"])
+ def tags_colors(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "tags_colors", project)
+
+ return response.Ok(dict(project.tags_colors))
+
+ @detail_route(methods=["POST"])
+ def create_tag(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "create_tag", project)
+ self._raise_if_blocked(project)
+
+ validator = validators.CreateTagValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+ services.create_tag(project, data.get("tag"), data.get("color"))
+
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def edit_tag(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "edit_tag", project)
+ self._raise_if_blocked(project)
+
+ validator = validators.EditTagTagValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+ services.edit_tag(project,
+ data.get("from_tag"),
+ to_tag=data.get("to_tag", None),
+ color=data.get("color", None))
+
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def delete_tag(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "delete_tag", project)
+ self._raise_if_blocked(project)
+
+ validator = validators.DeleteTagValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+ services.delete_tag(project, data.get("tag"))
+
+ return response.Ok()
+
+ @detail_route(methods=["POST"])
+ def mix_tags(self, request, pk=None):
+ project = self.get_object()
+ self.check_permissions(request, "mix_tags", project)
+ self._raise_if_blocked(project)
+
+ validator = validators.MixTagsValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+ services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
+
+ return response.Ok()
+
+
+class TaggedResourceMixin:
+ def pre_save(self, obj):
+ if obj.tags:
+ self._pre_save_new_tags_in_project_tagss_colors(obj)
+ super().pre_save(obj)
+
+ def _pre_save_new_tags_in_project_tagss_colors(self, obj):
+ new_obj_tags = OrderedSet()
+ new_tags_colors = {}
+
+ for tag in obj.tags:
+ if isinstance(tag, (list, tuple)):
+ name, color = tag
+
+ if color and not services.tag_exist_for_project_elements(obj.project, name):
+ new_tags_colors[name] = color
+
+ new_obj_tags.add(name)
+ elif isinstance(tag, str):
+ new_obj_tags.add(tag.lower())
+
+ obj.tags = list(new_obj_tags)
+
+ if new_tags_colors:
+ services.create_tags(obj.project, new_tags_colors)
diff --git a/taiga/projects/tagging/fields.py b/taiga/projects/tagging/fields.py
new file mode 100644
index 00000000..47553d8c
--- /dev/null
+++ b/taiga/projects/tagging/fields.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.forms import widgets
+from django.utils.translation import ugettext_lazy as _
+
+from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
+
+import re
+
+
+class TagsAndTagsColorsField(serializers.WritableField):
+ """
+ Pickle objects serializer fior stories, tasks and issues tags.
+ """
+ def __init__(self, *args, **kwargs):
+ def _validate_tag_field(value):
+ # Valid field:
+ # - ["tag1", "tag2", "tag3"...]
+ # - ["tag1", ["tag2", None], ["tag3", "#ccc"], [tag4, #cccccc]...]
+ for tag in value:
+ if isinstance(tag, str):
+ continue
+
+ if isinstance(tag, (list, tuple)) and len(tag) == 2:
+ name = tag[0]
+ color = tag[1]
+
+ if isinstance(name, str):
+ if color is None:
+ continue
+
+ if isinstance(color, str) and re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
+ continue
+
+ raise ValidationError(_("Invalid tag '{value}'. The color is not a "
+ "valid HEX color or null.").format(value=tag))
+
+ raise ValidationError(_("Invalid tag '{value}'. it must be the name or a pair "
+ "'[\"name\", \"hex color/\" | null]'.").format(value=tag))
+
+ super().__init__(*args, **kwargs)
+ self.validators.append(_validate_tag_field)
+
+ def to_native(self, obj):
+ return obj
+
+ def from_native(self, data):
+ return data
+
+
+class TagsField(serializers.WritableField):
+ """
+ Pickle objects serializer for tags names.
+ """
+ def __init__(self, *args, **kwargs):
+ def _validate_tag_field(value):
+ for tag in value:
+ if isinstance(tag, str):
+ continue
+ raise ValidationError(_("Invalid tag '{value}'. It must be the tag name.").format(value=tag))
+
+ super().__init__(*args, **kwargs)
+ self.validators.append(_validate_tag_field)
+
+ def to_native(self, obj):
+ return obj
+
+ def from_native(self, data):
+ return data
+
+
+class TagsColorsField(serializers.WritableField):
+ """
+ PgArray objects serializer.
+ """
+ widget = widgets.Textarea
+
+ def to_native(self, obj):
+ return dict(obj)
+
+ def from_native(self, data):
+ return list(data.items())
diff --git a/taiga/projects/tagging/models.py b/taiga/projects/tagging/models.py
new file mode 100644
index 00000000..970dae40
--- /dev/null
+++ b/taiga/projects/tagging/models.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.db import models
+from django.contrib.postgres.fields import ArrayField
+from django.utils.translation import ugettext_lazy as _
+
+
+class TaggedMixin(models.Model):
+ tags = ArrayField(models.TextField(),
+ null=True, blank=True, default=[], verbose_name=_("tags"))
+
+ class Meta:
+ abstract = True
+
+
+class TagsColorsdMixin(models.Model):
+ tags_colors = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2),
+ null=True, blank=True, default=[], verbose_name=_("tags colors"))
+
+ class Meta:
+ abstract = True
diff --git a/taiga/projects/likes/mixins/serializers.py b/taiga/projects/tagging/serializers.py
similarity index 73%
rename from taiga/projects/likes/mixins/serializers.py
rename to taiga/projects/tagging/serializers.py
index 84d63b4e..494b508a 100644
--- a/taiga/projects/likes/mixins/serializers.py
+++ b/taiga/projects/tagging/serializers.py
@@ -17,14 +17,15 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.fields import MethodField
-class FanResourceSerializerMixin(serializers.ModelSerializer):
- is_fan = serializers.SerializerMethodField("get_is_fan")
+class TaggedInProjectResourceSerializer(serializers.LightSerializer):
+ tags = MethodField()
- def get_is_fan(self, obj):
- if "request" in self.context:
- user = self.context["request"].user
- return user.is_authenticated() and user.is_fan(obj)
+ def get_tags(self, obj):
+ if not obj.tags:
+ return []
- return False
+ project_tag_colors = dict(obj.project.tags_colors)
+ return [[tag, project_tag_colors.get(tag, None)] for tag in obj.tags]
diff --git a/taiga/projects/tagging/services.py b/taiga/projects/tagging/services.py
new file mode 100644
index 00000000..43cf8567
--- /dev/null
+++ b/taiga/projects/tagging/services.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.db import connection
+
+
+def tag_exist_for_project_elements(project, tag):
+ return tag in dict(project.tags_colors).keys()
+
+
+def create_tags(project, new_tags_colors):
+ project.tags_colors += [[k, v] for k, v in new_tags_colors.items()]
+ project.save(update_fields=["tags_colors"])
+
+
+def create_tag(project, tag, color):
+ project.tags_colors.append([tag, color])
+ project.save(update_fields=["tags_colors"])
+
+
+def edit_tag(project, from_tag, to_tag, color):
+ print("edit_tag", project, from_tag, to_tag, color)
+ sql = """
+ UPDATE userstories_userstory
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE tasks_task
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE issues_issue
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE epics_epic
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+ """
+ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ tags_colors = dict(project.tags_colors)
+ tags_colors.pop(from_tag)
+ tags_colors[to_tag] = color
+ project.tags_colors = list(tags_colors.items())
+ project.save(update_fields=["tags_colors"])
+
+
+def rename_tag(project, from_tag, to_tag, **kwargs):
+ # Kwargs can have a color parameter
+ update_color = "color" in kwargs
+ if update_color:
+ color = kwargs.get("color")
+ else:
+ color = dict(project.tags_colors)[from_tag]
+ sql = """
+ UPDATE userstories_userstory
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE tasks_task
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE issues_issue
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+
+ UPDATE epics_epic
+ SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}'))
+ WHERE project_id = {project_id};
+ """
+ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ tags_colors = dict(project.tags_colors)
+ tags_colors.pop(from_tag)
+ tags_colors[to_tag] = color
+ project.tags_colors = list(tags_colors.items())
+ project.save(update_fields=["tags_colors"])
+
+
+def delete_tag(project, tag):
+ sql = """
+ UPDATE userstories_userstory
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
+
+ UPDATE tasks_task
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
+
+ UPDATE issues_issue
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
+
+ UPDATE epics_epic
+ SET tags = array_remove(tags, '{tag}')
+ WHERE project_id = {project_id};
+ """
+ sql = sql.format(project_id=project.id, tag=tag)
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ tags_colors = dict(project.tags_colors)
+ del tags_colors[tag]
+ project.tags_colors = list(tags_colors.items())
+ project.save(update_fields=["tags_colors"])
+
+
+def mix_tags(project, from_tags, to_tag):
+ color = dict(project.tags_colors)[to_tag]
+ for from_tag in from_tags:
+ rename_tag(project, from_tag, to_tag, color=color)
diff --git a/taiga/projects/tagging/signals.py b/taiga/projects/tagging/signals.py
new file mode 100644
index 00000000..cc94461a
--- /dev/null
+++ b/taiga/projects/tagging/signals.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+
+def tags_normalization(sender, instance, **kwargs):
+ if isinstance(instance.tags, (list, tuple)):
+ instance.tags = list(map(str.lower, instance.tags))
diff --git a/taiga/projects/tagging/validators.py b/taiga/projects/tagging/validators.py
new file mode 100644
index 00000000..ea0c32c8
--- /dev/null
+++ b/taiga/projects/tagging/validators.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.utils.translation import ugettext as _
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+
+from . import services
+from . import fields
+
+import re
+
+
+class ProjectTagValidator(validators.Validator):
+ def __init__(self, *args, **kwargs):
+ # Don't pass the extra project arg
+ self.project = kwargs.pop("project")
+
+ # Instantiate the superclass normally
+ super().__init__(*args, **kwargs)
+
+
+class CreateTagValidator(ProjectTagValidator):
+ tag = serializers.CharField()
+ color = serializers.CharField(required=False)
+
+ def validate_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if services.tag_exist_for_project_elements(self.project, tag):
+ raise ValidationError(_("This tag already exists."))
+
+ return attrs
+
+ def validate_color(self, attrs, source):
+ color = attrs.get(source, None)
+ if color is not None and not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
+ raise ValidationError(_("The color is not a valid HEX color."))
+
+ return attrs
+
+
+class EditTagTagValidator(ProjectTagValidator):
+ from_tag = serializers.CharField()
+ to_tag = serializers.CharField(required=False)
+ color = serializers.CharField(required=False)
+
+ def validate_from_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise ValidationError(_("The tag doesn't exist."))
+
+ return attrs
+
+ def validate_to_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if services.tag_exist_for_project_elements(self.project, tag):
+ raise ValidationError(_("This tag already exists."))
+
+ return attrs
+
+ def validate_color(self, attrs, source):
+ color = attrs.get(source, None)
+ if color and not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
+ raise ValidationError(_("The color is not a valid HEX color."))
+
+ return attrs
+
+ def validate(self, data):
+ if "to_tag" not in data:
+ data["to_tag"] = data.get("from_tag")
+
+ if "color" not in data:
+ data["color"] = dict(self.project.tags_colors).get(data.get("from_tag"))
+
+ return data
+
+
+class DeleteTagValidator(ProjectTagValidator):
+ tag = serializers.CharField()
+
+ def validate_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise ValidationError(_("The tag doesn't exist."))
+
+ return attrs
+
+
+class MixTagsValidator(ProjectTagValidator):
+ from_tags = fields.TagsField()
+ to_tag = serializers.CharField()
+
+ def validate_from_tags(self, attrs, source):
+ tags = attrs.get(source, None)
+ for tag in tags:
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise ValidationError(_("The tag doesn't exist."))
+
+ return attrs
+
+ def validate_to_tag(self, attrs, source):
+ tag = attrs.get(source, None)
+ if not services.tag_exist_for_project_elements(self.project, tag):
+ raise ValidationError(_("The tag doesn't exist."))
+
+ return attrs
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index d991b39b..778e080d 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.http import HttpResponse
from django.utils.translation import ugettext as _
from taiga.base.api.utils import get_object_or_404
@@ -24,29 +25,47 @@ from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
-from taiga.projects.models import Project, TaskStatus
-from django.http import HttpResponse
-
-from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
+from taiga.base.utils import json
from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.milestones.models import Milestone
+from taiga.projects.models import Project, TaskStatus
+from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
-from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
+from taiga.projects.tagging.api import TaggedResourceMixin
+from taiga.projects.userstories.models import UserStory
+from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from . import models
from . import permissions
from . import serializers
from . import services
+from . import validators
+from . import utils as tasks_utils
-class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- BlockedByProjectMixin, ModelCrudViewSet):
+class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
+ WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
+ ModelCrudViewSet):
+ validator_class = validators.TaskValidator
queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,)
- filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
- retrieve_exclude_filters = (filters.WatchersFilter,)
- filter_fields = ["user_story", "milestone", "project", "assigned_to",
- "status__is_closed"]
+ filter_backends = (filters.CanViewTasksFilterBackend,
+ filters.OwnersFilter,
+ filters.AssignedToFilter,
+ filters.StatusesFilter,
+ filters.TagsFilter,
+ filters.WatchersFilter,
+ filters.QFilter,
+ filters.CreatedDateFilter,
+ filters.ModifiedDateFilter,
+ filters.FinishedDateFilter)
+ filter_fields = ["user_story",
+ "milestone",
+ "project",
+ "project__slug",
+ "assigned_to",
+ "status__is_closed"]
def get_serializer_class(self, *args, **kwargs):
if self.action in ["retrieve", "by_ref"]:
@@ -57,17 +76,111 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
return serializers.TaskSerializer
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = qs.select_related("milestone",
+ "project",
+ "status",
+ "owner",
+ "assigned_to")
+
+ include_attachments = "include_attachments" in self.request.QUERY_PARAMS
+ qs = tasks_utils.attach_extra_info(qs, user=self.request.user,
+ include_attachments=include_attachments)
+
+ return qs
+
+ def pre_conditions_on_save(self, obj):
+ super().pre_conditions_on_save(obj)
+
+ if obj.milestone and obj.milestone.project != obj.project:
+ raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
+
+ if obj.user_story and obj.user_story.project != obj.project:
+ raise exc.WrongArguments(_("You don't have permissions to set this user story to this task."))
+
+ if obj.status and obj.status.project != obj.project:
+ raise exc.WrongArguments(_("You don't have permissions to set this status to this task."))
+
+ if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone:
+ raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
+
+ """
+ Updating some attributes of the userstory can affect the ordering in the backlog, kanban or taskboard
+ These two methods generate a key for the task and can be used to be compared before and after
+ saving
+ If there is any difference it means an extra ordering update must be done
+ """
+ def _us_order_key(self, obj):
+ return "{}-{}-{}".format(obj.project_id, obj.user_story_id, obj.us_order)
+
+ def _taskboard_order_key(self, obj):
+ return "{}-{}-{}-{}".format(obj.project_id, obj.user_story_id, obj.status_id, obj.taskboard_order)
+
+ def pre_save(self, obj):
+ if obj.user_story:
+ obj.milestone = obj.user_story.milestone
+ if not obj.id:
+ obj.owner = self.request.user
+ else:
+ self._old_us_order_key = self._us_order_key(self.get_object())
+ self._old_taskboard_order_key = self._taskboard_order_key(self.get_object())
+
+ super().pre_save(obj)
+
+ def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr,
+ project, user_story=None, status=None, milestone=None):
+ # Executes the extra ordering if there is a difference in the ordering keys
+ if old_order_key != order_key:
+ extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}"))
+ data = [{"task_id": obj.id, "order": getattr(obj, order_attr)}]
+ for id, order in extra_orders.items():
+ data.append({"task_id": int(id), "order": order})
+
+ return services.update_tasks_order_in_bulk(data,
+ order_attr,
+ project,
+ user_story=user_story,
+ status=status,
+ milestone=milestone)
+ return {}
+
+ def post_save(self, obj, created=False):
+ if not created:
+ # Let's reorder the related stuff after edit the element
+ orders_updated = {}
+ updated = self._reorder_if_needed(obj,
+ self._old_us_order_key,
+ self._us_order_key(obj),
+ "us_order",
+ obj.project,
+ user_story=obj.user_story)
+ orders_updated.update(updated)
+ updated = self._reorder_if_needed(obj,
+ self._old_taskboard_order_key,
+ self._taskboard_order_key(obj),
+ "taskboard_order",
+ obj.project,
+ user_story=obj.user_story,
+ status=obj.status,
+ milestone=obj.milestone)
+ orders_updated.update(updated)
+ self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated)
+
+ super().post_save(obj, created)
+
def update(self, request, *args, **kwargs):
self.object = self.get_object_or_none()
project_id = request.DATA.get('project', None)
+
if project_id and self.object and self.object.project.id != project_id:
try:
new_project = Project.objects.get(pk=project_id)
self.check_permissions(request, "destroy", self.object)
self.check_permissions(request, "create", new_project)
- sprint_id = request.DATA.get('milestone', None)
- if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0:
+ milestone_id = request.DATA.get('milestone', None)
+ if milestone_id is not None and new_project.milestones.filter(pk=milestone_id).count() == 0:
request.DATA['milestone'] = None
us_id = request.DATA.get('user_story', None)
@@ -88,46 +201,39 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
return super().update(request, *args, **kwargs)
- def get_queryset(self):
- qs = super().get_queryset()
- qs = self.attach_votes_attrs_to_queryset(qs)
- qs = qs.select_related(
- "milestone",
- "owner",
- "assigned_to",
- "status",
- "project")
+ @list_route(methods=["GET"])
+ def filters_data(self, request, *args, **kwargs):
+ project_id = request.QUERY_PARAMS.get("project", None)
+ project = get_object_or_404(Project, id=project_id)
- return self.attach_watchers_attrs_to_queryset(qs)
+ filter_backends = self.get_filter_backends()
+ statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
+ assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
+ owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
- def pre_save(self, obj):
- if obj.user_story:
- obj.milestone = obj.user_story.milestone
- if not obj.id:
- obj.owner = self.request.user
- super().pre_save(obj)
-
- def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
-
- if obj.milestone and obj.milestone.project != obj.project:
- raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
-
- if obj.user_story and obj.user_story.project != obj.project:
- raise exc.WrongArguments(_("You don't have permissions to set this user story to this task."))
-
- if obj.status and obj.status.project != obj.project:
- raise exc.WrongArguments(_("You don't have permissions to set this status to this task."))
-
- if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone:
- raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
+ queryset = self.get_queryset()
+ querysets = {
+ "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
+ "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
+ "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
+ "tags": self.filter_queryset(queryset)
+ }
+ return response.Ok(services.get_tasks_filters_data(project, querysets))
@list_route(methods=["GET"])
def by_ref(self, request):
- ref = request.QUERY_PARAMS.get("ref", None)
+ retrieve_kwargs = {
+ "ref": request.QUERY_PARAMS.get("ref", None)
+ }
project_id = request.QUERY_PARAMS.get("project", None)
- task = get_object_or_404(models.Task, ref=ref, project_id=project_id)
- return self.retrieve(request, pk=task.pk)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"])
def csv(self, request):
@@ -144,42 +250,64 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.TasksBulkSerializer(data=request.DATA)
- if serializer.is_valid():
- data = serializer.data
- 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"))
+ validator = validators.TasksBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- 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,
- project=project, owner=request.user, callback=self.post_save, precall=self.pre_save)
- tasks_serialized = self.get_serializer_class()(tasks, many=True)
+ data = validator.data
+ project = Project.objects.get(id=data["project_id"])
+ self.check_permissions(request, 'bulk_create', project)
+ if project.blocked_code is not None:
+ raise exc.Blocked(_("Blocked element"))
- return response.Ok(tasks_serialized.data)
+ tasks = services.create_tasks_in_bulk(
+ data["bulk_tasks"], milestone_id=data["milestone_id"], user_story_id=data["us_id"],
+ status_id=data.get("status_id") or project.default_task_status_id,
+ project=project, owner=request.user, callback=self.post_save, precall=self.pre_save)
+
+ tasks = self.get_queryset().filter(id__in=[i.id for i in tasks])
+ for task in tasks:
+ self.persist_history_snapshot(obj=task)
+
+ tasks_serialized = self.get_serializer_class()(tasks, many=True)
+
+ return response.Ok(tasks_serialized.data)
- return response.BadRequest(serializer.errors)
def _bulk_update_order(self, order_field, request, **kwargs):
- serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project)
if project.blocked_code is not None:
raise exc.Blocked(_("Blocked element"))
- services.update_tasks_order_in_bulk(data["bulk_tasks"],
- project=project,
- field=order_field)
- services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user)
+ user_story = None
+ user_story_id = data.get("user_story_id", None)
+ if user_story_id is not None:
+ user_story = get_object_or_404(UserStory, pk=user_story_id)
- return response.NoContent()
+ status = None
+ status_id = data.get("status_id", None)
+ if status_id is not None:
+ status = get_object_or_404(TaskStatus, pk=status_id)
+
+ milestone = None
+ milestone_id = data.get("milestone_id", None)
+ if milestone_id is not None:
+ milestone = get_object_or_404(Milestone, pk=milestone_id)
+
+ ret = services.update_tasks_order_in_bulk(data["bulk_tasks"],
+ order_field,
+ project,
+ user_story=user_story,
+ status=status,
+ milestone=milestone)
+ return response.Ok(ret)
@list_route(methods=["POST"])
def bulk_update_taskboard_order(self, request, **kwargs):
diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py
index 616854f6..1ad2e96d 100644
--- a/taiga/projects/tasks/apps.py
+++ b/taiga/projects/tasks/apps.py
@@ -22,22 +22,18 @@ from django.db.models import signals
def connect_tasks_signals():
- from taiga.projects import signals as generic_handlers
+ from taiga.projects.tagging import signals as tagging_handlers
from . import signals as handlers
+
# Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_task,
sender=apps.get_model("tasks", "Task"),
dispatch_uid="set_finished_date_when_edit_task")
# Tags
- signals.pre_save.connect(generic_handlers.tags_normalization,
+ signals.pre_save.connect(tagging_handlers.tags_normalization,
sender=apps.get_model("tasks", "Task"),
dispatch_uid="tags_normalization_task")
- signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
- sender=apps.get_model("tasks", "Task"),
- dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item_task")
- signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
- sender=apps.get_model("tasks", "Task"),
- dispatch_uid="update_project_tags_when_delete_tagglabe_item_task")
+
def connect_tasks_close_or_open_us_and_milestone_signals():
from . import signals as handlers
@@ -53,6 +49,7 @@ def connect_tasks_close_or_open_us_and_milestone_signals():
sender=apps.get_model("tasks", "Task"),
dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
+
def connect_tasks_custom_attributes_signals():
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task,
@@ -67,19 +64,24 @@ def connect_all_tasks_signals():
def disconnect_tasks_signals():
- signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization")
- signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item")
- signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_delete_tagglabe_item")
+ signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
+ dispatch_uid="set_finished_date_when_edit_task")
+ signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
+ dispatch_uid="tags_normalization")
def disconnect_tasks_close_or_open_us_and_milestone_signals():
- signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="cached_prev_task")
- signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task")
- signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
+ signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"),
+ dispatch_uid="cached_prev_task")
+ signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"),
+ dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task")
+ signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"),
+ dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task")
def disconnect_tasks_custom_attributes_signals():
- signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="create_custom_attribute_value_when_create_task")
+ signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"),
+ dispatch_uid="create_custom_attribute_value_when_create_task")
def disconnect_all_tasks_signals():
diff --git a/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py b/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py
new file mode 100644
index 00000000..f269735a
--- /dev/null
+++ b/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-14 12:01
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tasks', '0009_auto_20151104_1131'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='task',
+ name='external_reference',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=False, null=False), blank=True, default=None, null=True, size=None, verbose_name='external reference'),
+ ),
+ migrations.AlterField(
+ model_name='task',
+ name='tags',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
+ ),
+ ]
diff --git a/taiga/projects/tasks/migrations/0011_auto_20160928_0755.py b/taiga/projects/tasks/migrations/0011_auto_20160928_0755.py
new file mode 100644
index 00000000..1802a9c3
--- /dev/null
+++ b/taiga/projects/tasks/migrations/0011_auto_20160928_0755.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-28 07:55
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.time
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tasks', '0010_auto_20160614_1201'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='task',
+ name='taskboard_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='taskboard order'),
+ ),
+ migrations.AlterField(
+ model_name='task',
+ name='us_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='us order'),
+ ),
+ ]
diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py
index 30406387..a0abe570 100644
--- a/taiga/projects/tasks/models.py
+++ b/taiga/projects/tasks/models.py
@@ -18,16 +18,16 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.postgres.fields import ArrayField
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
-from djorm_pgarray.fields import TextArrayField
-
+from taiga.base.utils.time import timestamp_ms
from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.mixins.blocked import BlockedMixin
-from taiga.base.tags import TaggedMixin
+from taiga.projects.tagging.models import TaggedMixin
class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
@@ -54,9 +54,9 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M
subject = models.TextField(null=False, blank=False,
verbose_name=_("subject"))
- us_order = models.IntegerField(null=False, blank=False, default=1,
+ us_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
verbose_name=_("us order"))
- taskboard_order = models.IntegerField(null=False, blank=False, default=1,
+ taskboard_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
verbose_name=_("taskboard order"))
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
@@ -66,7 +66,8 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M
attachments = GenericRelation("attachments.Attachment")
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_("is iocaine"))
- external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
+ external_reference = ArrayField(models.TextField(null=False, blank=False),
+ null=True, blank=True, default=None, verbose_name=_("external reference"))
_importing = None
class Meta:
diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py
index 6a4fbc30..a1cbdfe1 100644
--- a/taiga/projects/tasks/permissions.py
+++ b/taiga/projects/tasks/permissions.py
@@ -16,9 +16,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectAdmin, AllowAny,
- IsSuperUser)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
class TaskPermission(TaigaResourcePermission):
@@ -26,10 +27,11 @@ class TaskPermission(TaigaResourcePermission):
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
create_perms = HasProjectPerm('add_task')
- update_perms = HasProjectPerm('modify_task')
- partial_update_perms = HasProjectPerm('modify_task')
+ update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task')
destroy_perms = HasProjectPerm('delete_task')
list_perms = AllowAny()
+ filters_data_perms = AllowAny()
csv_perms = AllowAny()
bulk_create_perms = HasProjectPerm('add_task')
bulk_update_order_perms = HasProjectPerm('modify_task')
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index a7c1c2a8..f0621581 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -17,97 +17,66 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-
-from taiga.base.fields import TagsField
-from taiga.base.fields import PgArrayField
-
+from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender
-from taiga.projects.validators import ProjectExistsValidator
-from taiga.projects.milestones.validators import SprintExistsValidator
-from taiga.projects.tasks.validators import TaskExistsValidator
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
+from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
+from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
-from taiga.users.serializers import UserBasicInfoSerializer
+class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
+ TaggedInProjectResourceSerializer, serializers.LightSerializer):
-from . import models
+ id = Field()
+ user_story = Field(attr="user_story_id")
+ ref = Field()
+ project = Field(attr="project_id")
+ milestone = Field(attr="milestone_id")
+ milestone_slug = MethodField()
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ us_order = Field()
+ taskboard_order = Field()
+ is_iocaine = Field()
+ external_reference = Field()
+ version = Field()
+ watchers = Field()
+ is_blocked = Field()
+ blocked_note = Field()
+ is_closed = MethodField()
+ user_story_extra_info = Field()
+
+ def get_milestone_slug(self, obj):
+ return obj.milestone.slug if obj.milestone else None
+
+ def get_is_closed(self, obj):
+ return obj.status is not None and obj.status.is_closed
-class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
- tags = TagsField(required=False, default=[])
- external_reference = PgArrayField(required=False)
- comment = serializers.SerializerMethodField("get_comment")
- milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
- blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
- description_html = serializers.SerializerMethodField("get_description_html")
- is_closed = serializers.SerializerMethodField("get_is_closed")
- status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True)
- assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
- owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
-
- class Meta:
- model = models.Task
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+class TaskSerializer(TaskListSerializer):
+ comment = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
def get_comment(self, obj):
return ""
- def get_milestone_slug(self, obj):
- if obj.milestone:
- return obj.milestone.slug
- else:
- return None
-
def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note)
def get_description_html(self, obj):
return mdrender(obj.project, obj.description)
- def get_is_closed(self, obj):
- return obj.status is not None and obj.status.is_closed
-
-
-class TaskListSerializer(TaskSerializer):
- class Meta:
- model = models.Task
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
- exclude=("description", "description_html")
-
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
- def serialize_neighbor(self, neighbor):
- if neighbor:
- return NeighborTaskSerializer(neighbor).data
- return None
-
-
-class NeighborTaskSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Task
- fields = ("id", "ref", "subject")
- depth = 0
-
-
-class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
- TaskExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- sprint_id = serializers.IntegerField()
- status_id = serializers.IntegerField(required=False)
- us_id = serializers.IntegerField(required=False)
- bulk_tasks = serializers.CharField()
-
-## Order bulk serializers
-
-class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
- task_id = serializers.IntegerField()
- order = serializers.IntegerField()
-
-
-class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_tasks = _TaskOrderBulkSerializer(many=True)
+ pass
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index 427e4f28..b785d373 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -16,14 +16,20 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import io
import csv
+import io
+from collections import OrderedDict
+from operator import itemgetter
+from contextlib import closing
+
+from django.db import connection
+from django.utils.translation import ugettext as _
from taiga.base.utils import db, text
from taiga.projects.history.services import take_snapshot
-from taiga.projects.tasks.apps import (
- connect_tasks_signals,
- disconnect_tasks_signals)
+from taiga.projects.services import apply_order_updates
+from taiga.projects.tasks.apps import connect_tasks_signals
+from taiga.projects.tasks.apps import disconnect_tasks_signals
from taiga.events import events
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.notifications.utils import attach_watchers_to_queryset
@@ -31,6 +37,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset
from . import models
+#####################################################
+# Bulk actions
+#####################################################
+
def get_tasks_from_bulk(bulk_data, **additional_fields):
"""Convert `bulk_data` into a list of tasks.
@@ -64,28 +74,36 @@ def create_tasks_in_bulk(bulk_data, callback=None, precall=None, **additional_fi
return tasks
-def update_tasks_order_in_bulk(bulk_data:list, field:str, project:object):
+def update_tasks_order_in_bulk(bulk_data: list, field: str, project: object,
+ user_story: object=None, status: object=None, milestone: object=None):
"""
- Update the order of some tasks.
- `bulk_data` should be a list of tuples with the following format:
+ Updates the order of the tasks specified adding the extra updates needed
+ to keep consistency.
- [(, {: , ...}), ...]
+ [{'task_id': , 'order': }, ...]
"""
- task_ids = []
- new_order_values = []
- for task_data in bulk_data:
- task_ids.append(task_data["task_id"])
- new_order_values.append({field: task_data["order"]})
+ tasks = project.tasks.all()
+ if user_story is not None:
+ tasks = tasks.filter(user_story=user_story)
+ if status is not None:
+ tasks = tasks.filter(status=status)
+ if milestone is not None:
+ tasks = tasks.filter(milestone=milestone)
+ task_orders = {task.id: getattr(task, field) for task in tasks}
+ new_task_orders = {e["task_id"]: e["order"] for e in bulk_data}
+ apply_order_updates(task_orders, new_task_orders)
+
+ task_ids = task_orders.keys()
events.emit_event_for_ids(ids=task_ids,
content_type="tasks.task",
projectid=project.pk)
- db.update_in_bulk_with_ids(task_ids, new_order_values, model=models.Task)
+ db.update_attr_in_bulk_for_ids(task_orders, field, models.Task)
+ return task_orders
def snapshot_tasks_in_bulk(bulk_data, user):
- task_ids = []
for task_data in bulk_data:
try:
task = models.Task.objects.get(pk=task_data['task_id'])
@@ -94,6 +112,10 @@ def snapshot_tasks_in_bulk(bulk_data, user):
pass
+#####################################################
+# CSV
+#####################################################
+
def tasks_to_csv(project, queryset):
csv_data = io.StringIO()
fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start",
@@ -144,7 +166,7 @@ def tasks_to_csv(project, queryset):
"voters": task.total_voters,
"created_date": task.created_date,
"modified_date": task.modified_date,
- "finished_date": task.finished_date,
+ "finished_date": task.finished_date,
}
for custom_attr in custom_attrs:
value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
@@ -153,3 +175,215 @@ def tasks_to_csv(project, queryset):
writer.writerow(task_data)
return csv_data
+
+
+#####################################################
+# Api filter data
+#####################################################
+
+def _get_tasks_statuses(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ SELECT "projects_taskstatus"."id",
+ "projects_taskstatus"."name",
+ "projects_taskstatus"."color",
+ "projects_taskstatus"."order",
+ (SELECT count(*)
+ FROM "tasks_task"
+ INNER JOIN "projects_project" ON
+ ("tasks_task"."project_id" = "projects_project"."id")
+ WHERE {where} AND "tasks_task"."status_id" = "projects_taskstatus"."id")
+ FROM "projects_taskstatus"
+ WHERE "projects_taskstatus"."project_id" = %s
+ ORDER BY "projects_taskstatus"."order";
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for id, name, color, order, count in rows:
+ result.append({
+ "id": id,
+ "name": _(name),
+ "color": color,
+ "order": order,
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("order"))
+
+
+def _get_tasks_assigned_to(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ WITH counters AS (
+ SELECT assigned_to_id, count(assigned_to_id) count
+ FROM "tasks_task"
+ INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id")
+ WHERE {where} AND "tasks_task"."assigned_to_id" IS NOT NULL
+ GROUP BY assigned_to_id
+ )
+
+ SELECT "projects_membership"."user_id" user_id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
+ LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
+
+ -- unassigned tasks
+ UNION
+
+ SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
+ FROM "tasks_task"
+ INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id")
+ WHERE {where} AND "tasks_task"."assigned_to_id" IS NULL
+ GROUP BY assigned_to_id
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id] + where_params)
+ rows = cursor.fetchall()
+
+ result = []
+ none_valued_added = False
+ for id, full_name, username, count in rows:
+ result.append({
+ "id": id,
+ "full_name": full_name or username or "",
+ "count": count,
+ })
+
+ if id is None:
+ none_valued_added = True
+
+ # If there was no task with null assigned_to we manually add it
+ if not none_valued_added:
+ result.append({
+ "id": None,
+ "full_name": "",
+ "count": 0,
+ })
+
+ return sorted(result, key=itemgetter("full_name"))
+
+
+def _get_tasks_owners(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ WITH counters AS (
+ SELECT "tasks_task"."owner_id" owner_id,
+ count(coalesce("tasks_task"."owner_id", -1)) count
+ FROM "tasks_task"
+ INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY "tasks_task"."owner_id"
+ )
+
+ SELECT "projects_membership"."user_id" id,
+ "users_user"."full_name",
+ "users_user"."username",
+ COALESCE("counters".count, 0) count
+ FROM projects_membership
+ LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
+ INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
+
+ -- System users
+ UNION
+
+ SELECT "users_user"."id" user_id,
+ "users_user"."full_name" full_name,
+ "users_user"."username" username,
+ COALESCE("counters".count, 0) count
+ FROM users_user
+ LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
+ WHERE ("users_user"."is_system" IS TRUE)
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for id, full_name, username, count in rows:
+ if count > 0:
+ result.append({
+ "id": id,
+ "full_name": full_name or username or "",
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("full_name"))
+
+
+def _get_tasks_tags(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+
+ extra_sql = """
+ WITH tasks_tags AS (
+ SELECT tag,
+ COUNT(tag) counter FROM (
+ SELECT UNNEST(tasks_task.tags) tag
+ FROM tasks_task
+ INNER JOIN projects_project
+ ON (tasks_task.project_id = projects_project.id)
+ WHERE {where}) tags
+ GROUP BY tag),
+ project_tags AS (
+ SELECT reduce_dim(tags_colors) tag_color
+ FROM projects_project
+ WHERE id=%s)
+
+ SELECT tag_color[1] tag,
+ tag_color[2] color,
+ COALESCE(tasks_tags.counter, 0) counter
+ FROM project_tags
+ LEFT JOIN tasks_tags ON project_tags.tag_color[1] = tasks_tags.tag
+ ORDER BY tag
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for name, color, count in rows:
+ result.append({
+ "name": name,
+ "color": color,
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("name"))
+
+
+def get_tasks_filters_data(project, querysets):
+ """
+ Given a project and an tasks queryset, return a simple data structure
+ of all possible filters for the tasks in the queryset.
+ """
+ data = OrderedDict([
+ ("statuses", _get_tasks_statuses(project, querysets["statuses"])),
+ ("assigned_to", _get_tasks_assigned_to(project, querysets["assigned_to"])),
+ ("owners", _get_tasks_owners(project, querysets["owners"])),
+ ("tags", _get_tasks_tags(project, querysets["tags"])),
+ ])
+
+ return data
diff --git a/taiga/projects/tasks/utils.py b/taiga/projects/tasks/utils.py
new file mode 100644
index 00000000..0d8661fc
--- /dev/null
+++ b/taiga/projects/tasks/utils.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.attachments.utils import attach_basic_attachments
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_user_story_extra_info(queryset, as_field="user_story_extra_info"):
+ """Attach userstory extra info as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the userstory extra info as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ sql = """SELECT row_to_json(u)
+ FROM (SELECT "userstories_userstory"."id" AS "id",
+ "userstories_userstory"."ref" AS "ref",
+ "userstories_userstory"."subject" AS "subject",
+ (SELECT json_agg(row_to_json(t))
+ FROM (SELECT "epics_epic"."id" AS "id",
+ "epics_epic"."ref" AS "ref",
+ "epics_epic"."subject" AS "subject",
+ "epics_epic"."color" AS "color",
+ (SELECT row_to_json(p)
+ FROM (SELECT "projects_project"."id" AS "id",
+ "projects_project"."name" AS "name",
+ "projects_project"."slug" AS "slug"
+ ) p
+ ) AS "project"
+ FROM "epics_relateduserstory"
+ INNER JOIN "epics_epic"
+ ON "epics_epic"."id" = "epics_relateduserstory"."epic_id"
+ INNER JOIN "projects_project"
+ ON "projects_project"."id" = "epics_epic"."project_id"
+ WHERE "epics_relateduserstory"."user_story_id" = "{tbl}"."user_story_id"
+ ORDER BY "projects_project"."name", "epics_epic"."ref") t) AS "epics"
+ FROM "userstories_userstory"
+ WHERE "userstories_userstory"."id" = "{tbl}"."user_story_id") u"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False):
+ if include_attachments:
+ queryset = attach_basic_attachments(queryset)
+ queryset = queryset.extra(select={"include_attachments": "True"})
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ queryset = attach_user_story_extra_info(queryset)
+ return queryset
diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py
index 4a100779..b9061bde 100644
--- a/taiga/projects/tasks/validators.py
+++ b/taiga/projects/tasks/validators.py
@@ -19,14 +19,135 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import PgArrayField
+from taiga.projects.milestones.models import Milestone
+from taiga.projects.models import TaskStatus
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.userstories.models import UserStory
+from taiga.projects.validators import ProjectExistsValidator
from . import models
-class TaskExistsValidator:
- def validate_task_id(self, attrs, source):
- value = attrs[source]
- if not models.Task.objects.filter(pk=value).exists():
- msg = _("There's no task with that id")
- raise serializers.ValidationError(msg)
+class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+
+ class Meta:
+ model = models.Task
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+
+class TasksBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ milestone_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ us_id = serializers.IntegerField(required=False)
+ bulk_tasks = serializers.CharField()
+
+ def validate_milestone_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+
+ if not Milestone.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid milestone id."))
+
+ return attrs
+
+ def validate_status_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+
+ if not TaskStatus.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid task status id."))
+
+ return attrs
+
+ def validate_us_id(self, attrs, source):
+ filters = {"project__id": attrs["project_id"]}
+
+ if "milestone_id" in attrs:
+ filters["milestone__id"] = attrs["milestone_id"]
+
+ filters["id"] = attrs["us_id"]
+
+ if not UserStory.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid user story id."))
+
+ return attrs
+
+
+# Order bulk validators
+
+class _TaskOrderBulkValidator(validators.Validator):
+ task_id = serializers.IntegerField()
+ order = serializers.IntegerField()
+
+
+class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ us_id = serializers.IntegerField(required=False)
+ milestone_id = serializers.IntegerField(required=False)
+ bulk_tasks = _TaskOrderBulkValidator(many=True)
+
+ def validate_status_id(self, attrs, source):
+ filters = {"project__id": attrs["project_id"]}
+ filters["id"] = attrs[source]
+
+ if not TaskStatus.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid task status id. The status must belong to "
+ "the same project."))
+
+ return attrs
+
+ def validate_us_id(self, attrs, source):
+ filters = {"project__id": attrs["project_id"]}
+
+ if "milestone_id" in attrs:
+ filters["milestone__id"] = attrs["milestone_id"]
+
+ filters["id"] = attrs[source]
+
+ if not UserStory.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid user story id. The user story must belong to "
+ "the same project."))
+
+ return attrs
+
+ def validate_milestone_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+
+ if not Milestone.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid milestone id. The milestone must belong to "
+ "the same project."))
+
+ return attrs
+
+ def validate_bulk_tasks(self, attrs, source):
+ filters = {"project__id": attrs["project_id"]}
+ if "status_id" in attrs:
+ filters["status__id"] = attrs["status_id"]
+ if "us_id" in attrs:
+ filters["user_story__id"] = attrs["us_id"]
+ if "milestone_id" in attrs:
+ filters["milestone__id"] = attrs["milestone_id"]
+
+ filters["id__in"] = [t["task_id"] for t in attrs[source]]
+
+ if models.Task.objects.filter(**filters).count() != len(filters["id__in"]):
+ raise ValidationError(_("Invalid task ids. All tasks must belong to the same project and, "
+ "if it exists, to the same status, user story and/or milestone."))
+
return attrs
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 83d8cb99..6ee1d72c 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -18,50 +18,60 @@
from django.apps import apps
from django.db import transaction
+
from django.utils.translation import ugettext as _
-from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
-from taiga.base import filters
+from taiga.base import filters as base_filters
from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base import status
from taiga.base.decorators import list_route
from taiga.base.api.mixins import BlockedByProjectMixin
-from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404
+from taiga.base.utils import json
-from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
-from taiga.projects.occ import OCCResourceMixin
-from taiga.projects.models import Project, UserStoryStatus
from taiga.projects.history.services import take_snapshot
-from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
+from taiga.projects.milestones.models import Milestone
+from taiga.projects.models import Project, UserStoryStatus
+from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchersViewSetMixin
+from taiga.projects.occ import OCCResourceMixin
+from taiga.projects.tagging.api import TaggedResourceMixin
+from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
+from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
+from taiga.projects.userstories.utils import attach_extra_info
+from . import filters
from . import models
from . import permissions
from . import serializers
from . import services
+from . import validators
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- BlockedByProjectMixin, ModelCrudViewSet):
+ TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
+ validator_class = validators.UserStoryValidator
queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,)
- filter_backends = (filters.CanViewUsFilterBackend,
- filters.OwnersFilter,
- filters.AssignedToFilter,
- filters.StatusesFilter,
- filters.TagsFilter,
- filters.WatchersFilter,
- filters.QFilter,
- filters.OrderByFilterMixin)
- retrieve_exclude_filters = (filters.OwnersFilter,
- filters.AssignedToFilter,
- filters.StatusesFilter,
- filters.TagsFilter,
- filters.WatchersFilter)
+ filter_backends = (base_filters.CanViewUsFilterBackend,
+ filters.EpicFilter,
+ base_filters.OwnersFilter,
+ base_filters.AssignedToFilter,
+ base_filters.StatusesFilter,
+ base_filters.TagsFilter,
+ base_filters.WatchersFilter,
+ base_filters.QFilter,
+ base_filters.CreatedDateFilter,
+ base_filters.ModifiedDateFilter,
+ base_filters.FinishDateFilter,
+ base_filters.OrderByFilterMixin)
filter_fields = ["project",
+ "project__slug",
"milestone",
"milestone__isnull",
"is_closed",
@@ -70,11 +80,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
order_by_fields = ["backlog_order",
"sprint_order",
"kanban_order",
+ "epic_order",
"total_voters"]
- # Specific filter used for filtering neighbor user stories
- _neighbor_tags_filter = filters.TagsFilter('neighbor_tags')
-
def get_serializer_class(self, *args, **kwargs):
if self.action in ["retrieve", "by_ref"]:
return serializers.UserStoryNeighborsSerializer
@@ -84,9 +92,165 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return serializers.UserStorySerializer
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = qs.select_related("milestone",
+ "project",
+ "status",
+ "owner",
+ "assigned_to",
+ "generated_from_issue")
+
+ include_attachments = "include_attachments" in self.request.QUERY_PARAMS
+ include_tasks = "include_tasks" in self.request.QUERY_PARAMS
+ epic_id = self.request.QUERY_PARAMS.get("epic", None)
+ # We can be filtering by more than one epic so epic_id can consist
+ # of different ids separete by comma. In that situation we will use
+ # only the first
+ if epic_id is not None:
+ epic_id = epic_id.split(",")[0]
+
+ qs = attach_extra_info(qs, user=self.request.user,
+ include_attachments=include_attachments,
+ include_tasks=include_tasks,
+ epic_id=epic_id)
+
+ return qs
+
+ def pre_conditions_on_save(self, obj):
+ super().pre_conditions_on_save(obj)
+
+ if obj.milestone and obj.milestone.project != obj.project:
+ raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
+ "to this user story."))
+
+ if obj.status and obj.status.project != obj.project:
+ raise exc.PermissionDenied(_("You don't have permissions to set this status "
+ "to this user story."))
+
+ """
+ Updating some attributes of the userstory can affect the ordering in the backlog, kanban or taskboard
+ These three methods generate a key for the user story and can be used to be compared before and after
+ saving
+ If there is any difference it means an extra ordering update must be done
+ """
+ def _backlog_order_key(self, obj):
+ return "{}-{}".format(obj.project_id, obj.backlog_order)
+
+ def _kanban_order_key(self, obj):
+ return "{}-{}-{}".format(obj.project_id, obj.status_id, obj.kanban_order)
+
+ def _sprint_order_key(self, obj):
+ return "{}-{}-{}".format(obj.project_id, obj.milestone_id, obj.sprint_order)
+
+ def pre_save(self, obj):
+ # This is very ugly hack, but having
+ # restframework is the only way to do it.
+ #
+ # NOTE: code moved as is from serializer
+ # to api because is not serializer logic.
+ related_data = getattr(obj, "_related_data", {})
+ self._role_points = related_data.pop("role_points", None)
+
+ if not obj.id:
+ obj.owner = self.request.user
+ else:
+ self._old_backlog_order_key = self._backlog_order_key(self.get_object())
+ self._old_kanban_order_key = self._kanban_order_key(self.get_object())
+ self._old_sprint_order_key = self._sprint_order_key(self.get_object())
+
+ super().pre_save(obj)
+
+ def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr,
+ project, status=None, milestone=None):
+ # Executes the extra ordering if there is a difference in the ordering keys
+ if old_order_key != order_key:
+ extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}"))
+ data = [{"us_id": obj.id, "order": getattr(obj, order_attr)}]
+ for id, order in extra_orders.items():
+ data.append({"us_id": int(id), "order": order})
+
+ return services.update_userstories_order_in_bulk(data,
+ order_attr,
+ project,
+ status=status,
+ milestone=milestone)
+ return {}
+
+ def post_save(self, obj, created=False):
+ if not created:
+ # Let's reorder the related stuff after edit the element
+ orders_updated = {}
+ updated = self._reorder_if_needed(obj,
+ self._old_backlog_order_key,
+ self._backlog_order_key(obj),
+ "backlog_order",
+ obj.project)
+ orders_updated.update(updated)
+ updated = self._reorder_if_needed(obj,
+ self._old_kanban_order_key,
+ self._kanban_order_key(obj),
+ "kanban_order",
+ obj.project,
+ status=obj.status)
+ orders_updated.update(updated)
+ updated = self._reorder_if_needed(obj,
+ self._old_sprint_order_key,
+ self._sprint_order_key(obj),
+ "sprint_order",
+ obj.project,
+ milestone=obj.milestone)
+ orders_updated.update(updated)
+ self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated)
+
+ # Code related to the hack of pre_save method.
+ # Rather, this is the continuation of it.
+ if self._role_points:
+ Points = apps.get_model("projects", "Points")
+ RolePoints = apps.get_model("userstories", "RolePoints")
+
+ for role_id, points_id in self._role_points.items():
+ try:
+ role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk,
+ role__computable=True)
+ except (ValueError, RolePoints.DoesNotExist):
+ raise exc.BadRequest({
+ "points": _("Invalid role id '{role_id}'").format(role_id=role_id)
+ })
+
+ try:
+ role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id)
+ except (ValueError, Points.DoesNotExist):
+ raise exc.BadRequest({
+ "points": _("Invalid points id '{points_id}'").format(points_id=points_id)
+ })
+
+ role_points.save()
+
+ super().post_save(obj, created)
+
+ @transaction.atomic
+ def create(self, *args, **kwargs):
+ response = super().create(*args, **kwargs)
+
+ # Added comment to the origin (issue)
+ if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue:
+ self.object.generated_from_issue.save()
+
+ comment = _("Generating the user story #{ref} - {subject}")
+ comment = comment.format(ref=self.object.ref, subject=self.object.subject)
+ history = take_snapshot(self.object.generated_from_issue,
+ comment=comment,
+ user=self.request.user)
+
+ self.send_notifications(self.object.generated_from_issue, history)
+
+ return response
+
def update(self, request, *args, **kwargs):
self.object = self.get_object_or_none()
project_id = request.DATA.get('project', None)
+
if project_id and self.object and self.object.project.id != project_id:
try:
new_project = Project.objects.get(pk=project_id)
@@ -110,95 +274,47 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return super().update(request, *args, **kwargs)
-
- def get_queryset(self):
- qs = super().get_queryset()
- qs = qs.prefetch_related("role_points",
- "role_points__points",
- "role_points__role")
- qs = qs.select_related("milestone",
- "project",
- "status",
- "owner",
- "assigned_to",
- "generated_from_issue")
- qs = self.attach_votes_attrs_to_queryset(qs)
- return self.attach_watchers_attrs_to_queryset(qs)
-
- def pre_save(self, obj):
- # This is very ugly hack, but having
- # restframework is the only way to do it.
- # NOTE: code moved as is from serializer
- # to api because is not serializer logic.
- related_data = getattr(obj, "_related_data", {})
- self._role_points = related_data.pop("role_points", None)
-
- if not obj.id:
- obj.owner = self.request.user
-
- super().pre_save(obj)
-
- def post_save(self, obj, created=False):
- # Code related to the hack of pre_save method. Rather, this is the continuation of it.
- if self._role_points:
- Points = apps.get_model("projects", "Points")
- RolePoints = apps.get_model("userstories", "RolePoints")
-
- for role_id, points_id in self._role_points.items():
- try:
- role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk,
- role__computable=True)
- except (ValueError, RolePoints.DoesNotExist):
- raise exc.BadRequest({"points": _("Invalid role id '{role_id}'").format(
- role_id=role_id)})
-
- try:
- role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id)
- except (ValueError, Points.DoesNotExist):
- raise exc.BadRequest({"points": _("Invalid points id '{points_id}'").format(
- points_id=points_id)})
-
- role_points.save()
-
- super().post_save(obj, created)
-
- def pre_conditions_on_save(self, obj):
- super().pre_conditions_on_save(obj)
-
- if obj.milestone and obj.milestone.project != obj.project:
- raise exc.PermissionDenied(_("You don't have permissions to set this sprint "
- "to this user story."))
-
- if obj.status and obj.status.project != obj.project:
- raise exc.PermissionDenied(_("You don't have permissions to set this status "
- "to this user story."))
-
@list_route(methods=["GET"])
def filters_data(self, request, *args, **kwargs):
project_id = request.QUERY_PARAMS.get("project", None)
project = get_object_or_404(Project, id=project_id)
filter_backends = self.get_filter_backends()
- statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
- assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
- owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
- tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter)
+ statuses_filter_backends = (f for f in filter_backends if f != base_filters.StatusesFilter)
+ assigned_to_filter_backends = (f for f in filter_backends if f != base_filters.AssignedToFilter)
+ owners_filter_backends = (f for f in filter_backends if f != base_filters.OwnersFilter)
+ epics_filter_backends = (f for f in filter_backends if f != filters.EpicFilter)
queryset = self.get_queryset()
querysets = {
"statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
"assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
- "tags": self.filter_queryset(queryset)
+ "tags": self.filter_queryset(queryset),
+ "epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends)
}
return response.Ok(services.get_userstories_filters_data(project, querysets))
@list_route(methods=["GET"])
def by_ref(self, request):
- ref = request.QUERY_PARAMS.get("ref", None)
+ if "ref" not in request.QUERY_PARAMS:
+ return response.BadRequest(_("ref param is needed"))
+
+ if "project_slug" not in request.QUERY_PARAMS and "project" not in request.QUERY_PARAMS:
+ return response.BadRequest(_("project or project_slug param is needed"))
+
+ retrieve_kwargs = {
+ "ref": request.QUERY_PARAMS["ref"]
+ }
project_id = request.QUERY_PARAMS.get("project", None)
- userstory = get_object_or_404(models.UserStory, ref=ref, project_id=project_id)
- return self.retrieve(request, pk=userstory.pk)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"])
def csv(self, request):
@@ -215,9 +331,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.UserStoriesBulkSerializer(data=request.DATA)
- if serializer.is_valid():
- data = serializer.data
+ validator = validators.UserStoriesBulkValidator(data=request.DATA)
+ if validator.is_valid():
+ data = validator.data
project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None:
@@ -227,28 +343,60 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
data["bulk_stories"], project=project, owner=request.user,
status_id=data.get("status_id") or project.default_us_status_id,
callback=self.post_save, precall=self.pre_save)
+
+ user_stories = self.get_queryset().filter(id__in=[i.id for i in user_stories])
+ for user_story in user_stories:
+ self.persist_history_snapshot(obj=user_story)
+
user_stories_serialized = self.get_serializer_class()(user_stories, many=True)
+
return response.Ok(user_stories_serialized.data)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
+
+ @list_route(methods=["POST"])
+ def bulk_update_milestone(self, request, **kwargs):
+ validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
+
+ data = validator.data
+ project = get_object_or_404(Project, pk=data["project_id"])
+ milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
+
+ self.check_permissions(request, "bulk_update_milestone", project)
+
+ services.update_userstories_milestone_in_bulk(data["bulk_stories"], milestone)
+ services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user)
+
+ return response.NoContent()
def _bulk_update_order(self, order_field, request, **kwargs):
- serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateUserStoriesOrderBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
+ status = None
+ status_id = data.get("status_id", None)
+ if status_id is not None:
+ status = get_object_or_404(UserStoryStatus, pk=status_id)
+
+ milestone = None
+ milestone_id = data.get("milestone_id", None)
+ if milestone_id is not None:
+ milestone = get_object_or_404(Milestone, pk=milestone_id)
self.check_permissions(request, "bulk_update_order", project)
if project.blocked_code is not None:
raise exc.Blocked(_("Blocked element"))
- services.update_userstories_order_in_bulk(data["bulk_stories"],
- project=project,
- field=order_field)
- services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user)
-
- return response.NoContent()
+ ret = services.update_userstories_order_in_bulk(data["bulk_stories"],
+ order_field,
+ project,
+ status=status,
+ milestone=milestone)
+ return response.Ok(ret)
@list_route(methods=["POST"])
def bulk_update_backlog_order(self, request, **kwargs):
@@ -262,23 +410,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
def bulk_update_kanban_order(self, request, **kwargs):
return self._bulk_update_order("kanban_order", request, **kwargs)
- @transaction.atomic
- def create(self, *args, **kwargs):
- response = super().create(*args, **kwargs)
-
- # Added comment to the origin (issue)
- if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue:
- self.object.generated_from_issue.save()
-
- comment = _("Generating the user story #{ref} - {subject}")
- comment = comment.format(ref=self.object.ref, subject=self.object.subject)
- history = take_snapshot(self.object.generated_from_issue,
- comment=comment,
- user=self.request.user)
-
- self.send_notifications(self.object.generated_from_issue, history)
-
- return response
class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.UserStoryVotersPermission,)
diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py
index ef3d5df5..d2fa8ce1 100644
--- a/taiga/projects/userstories/apps.py
+++ b/taiga/projects/userstories/apps.py
@@ -22,7 +22,7 @@ from django.db.models import signals
def connect_userstories_signals():
- from taiga.projects import signals as generic_handlers
+ from taiga.projects.tagging import signals as tagging_handlers
from . import signals as handlers
# When deleting user stories we must disable task signals while delating and
@@ -32,8 +32,8 @@ def connect_userstories_signals():
dispatch_uid='disable_task_signals')
signals.post_delete.connect(handlers.enable_tasks_signals,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid='enable_tasks_signals')
+ sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid='enable_tasks_signals')
# Cached prev object version
signals.pre_save.connect(handlers.cached_prev_us,
@@ -59,15 +59,9 @@ def connect_userstories_signals():
dispatch_uid="try_to_close_milestone_when_delete_us")
# Tags
- signals.pre_save.connect(generic_handlers.tags_normalization,
+ signals.pre_save.connect(tagging_handlers.tags_normalization,
sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="tags_normalization_user_story")
- signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
- signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
- sender=apps.get_model("userstories", "UserStory"),
- dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
def connect_userstories_custom_attributes_signals():
@@ -83,18 +77,27 @@ def connect_all_userstories_signals():
def disconnect_userstories_signals():
- signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
- signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us")
- signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story")
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story")
- signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story")
+ signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="cached_prev_us")
+
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="update_role_points_when_create_or_edit_us")
+
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="update_milestone_of_tasks_when_edit_us")
+
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us")
+ signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="try_to_close_milestone_when_delete_us")
+
+ signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="tags_normalization_user_story")
def disconnect_userstories_custom_attributes_signals():
- signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story")
+ signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"),
+ dispatch_uid="create_custom_attribute_value_when_create_user_story")
def disconnect_all_userstories_signals():
diff --git a/tests/unit/test_gravatar.py b/taiga/projects/userstories/filters.py
similarity index 73%
rename from tests/unit/test_gravatar.py
rename to taiga/projects/userstories/filters.py
index b6246fa8..ec19b6a7 100644
--- a/tests/unit/test_gravatar.py
+++ b/taiga/projects/userstories/filters.py
@@ -16,16 +16,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import hashlib
-from taiga.users.gravatar import get_gravatar_url
+from taiga.base import filters
-def test_get_gravatar_url():
- email = "user@email.com"
- email_hash = hashlib.md5(email.encode()).hexdigest()
- url = get_gravatar_url(email, s=40, d="default-image-url")
-
- assert email_hash in url
- assert 's=40' in url
- assert 'd=default-image-url' in url
+class EpicFilter(filters.BaseRelatedFieldsFilter):
+ filter_name = "epics"
+ param_name = "epic"
diff --git a/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py b/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py
new file mode 100644
index 00000000..fd0fe25c
--- /dev/null
+++ b/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-14 12:01
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('userstories', '0011_userstory_tribe_gig'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userstory',
+ name='external_reference',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=False, null=False), blank=True, default=None, null=True, size=None, verbose_name='external reference'),
+ ),
+ migrations.AlterField(
+ model_name='userstory',
+ name='tags',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'),
+ ),
+ ]
diff --git a/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py b/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py
new file mode 100644
index 00000000..64a73be8
--- /dev/null
+++ b/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-22 10:18
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('userstories', '0012_auto_20160614_1201'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userstory',
+ name='kanban_order',
+ field=models.IntegerField(default=10000, verbose_name='kanban order'),
+ ),
+ ]
diff --git a/taiga/projects/userstories/migrations/0014_auto_20160928_0540.py b/taiga/projects/userstories/migrations/0014_auto_20160928_0540.py
new file mode 100644
index 00000000..38285839
--- /dev/null
+++ b/taiga/projects/userstories/migrations/0014_auto_20160928_0540.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-28 05:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.time
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('userstories', '0013_auto_20160722_1018'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userstory',
+ name='backlog_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='backlog order'),
+ ),
+ migrations.AlterField(
+ model_name='userstory',
+ name='kanban_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='kanban order'),
+ ),
+ migrations.AlterField(
+ model_name='userstory',
+ name='sprint_order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='sprint order'),
+ ),
+ ]
diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py
index 86332b46..178f2cc1 100644
--- a/taiga/projects/userstories/models.py
+++ b/taiga/projects/userstories/models.py
@@ -18,14 +18,15 @@
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.postgres.fields import ArrayField
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
-from djorm_pgarray.fields import TextArrayField
from picklefield.fields import PickledObjectField
-from taiga.base.tags import TaggedMixin
+from taiga.base.utils.time import timestamp_ms
+from taiga.projects.tagging.models import TaggedMixin
from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.mixins.blocked import BlockedMixin
@@ -55,6 +56,7 @@ class RolePoints(models.Model):
def project(self):
return self.user_story.project
+
class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model):
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
verbose_name=_("ref"))
@@ -74,12 +76,12 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
related_name="userstories", through="RolePoints",
verbose_name=_("points"))
- backlog_order = models.IntegerField(null=False, blank=False, default=10000,
+ backlog_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
verbose_name=_("backlog order"))
- sprint_order = models.IntegerField(null=False, blank=False, default=10000,
- verbose_name=_("sprint order"))
- kanban_order = models.IntegerField(null=False, blank=False, default=10000,
+ sprint_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
verbose_name=_("sprint order"))
+ kanban_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
+ verbose_name=_("kanban order"))
created_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("created date"),
@@ -103,7 +105,8 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod
on_delete=models.SET_NULL,
related_name="generated_user_stories",
verbose_name=_("generated from issue"))
- external_reference = TextArrayField(default=None, verbose_name=_("external reference"))
+ external_reference = ArrayField(models.TextField(null=False, blank=False),
+ null=True, blank=True, default=None, verbose_name=_("external reference"))
tribe_gig = PickledObjectField(null=True, blank=True, default=None,
verbose_name="taiga tribe gig")
diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py
index 8148d524..2d200446 100644
--- a/taiga/projects/userstories/permissions.py
+++ b/taiga/projects/userstories/permissions.py
@@ -16,22 +16,27 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- IsAuthenticated, IsProjectAdmin,
- AllowAny, IsSuperUser)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
+
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
class UserStoryPermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
retrieve_perms = HasProjectPerm('view_us')
+ by_ref_perms = HasProjectPerm('view_us')
create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')
- update_perms = HasProjectPerm('modify_us')
- partial_update_perms = HasProjectPerm('modify_us')
+ update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us')
destroy_perms = HasProjectPerm('delete_us')
list_perms = AllowAny()
filters_data_perms = AllowAny()
csv_perms = AllowAny()
bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us'))
bulk_update_order_perms = HasProjectPerm('modify_us')
+ bulk_update_milestone_perms = HasProjectPerm('modify_us')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_us')
watch_perms = IsAuthenticated() & HasProjectPerm('view_us')
diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py
index 7bfa7142..b7c43466 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -16,88 +16,128 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.apps import apps
from taiga.base.api import serializers
-from taiga.base.fields import TagsField
-from taiga.base.fields import PickledObjectField
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin
-from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender
-from taiga.projects.validators import ProjectExistsValidator
-from taiga.projects.validators import UserStoryStatusExistsValidator
-from taiga.projects.userstories.validators import UserStoryExistsValidator
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.serializers import BasicUserStoryStatusSerializer
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
+from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
+from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
-from taiga.users.serializers import UserBasicInfoSerializer
-from . import models
+class OriginIssueSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+
+ def to_value(self, instance):
+ if instance is None:
+ return None
+
+ return super().to_value(instance)
-class RolePointsField(serializers.WritableField):
- def to_native(self, obj):
- return {str(o.role.id): o.points.id for o in obj.all()}
+class UserStoryListSerializer(ProjectExtraInfoSerializerMixin,
+ VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
+ TaggedInProjectResourceSerializer,
+ serializers.LightSerializer):
- def from_native(self, obj):
- if isinstance(obj, dict):
- return obj
- return json.loads(obj)
+ id = Field()
+ ref = Field()
+ milestone = Field(attr="milestone_id")
+ milestone_slug = MethodField()
+ milestone_name = MethodField()
+ project = Field(attr="project_id")
+ is_closed = Field()
+ points = MethodField()
+ backlog_order = Field()
+ sprint_order = Field()
+ kanban_order = Field()
+ created_date = Field()
+ modified_date = Field()
+ finish_date = Field()
+ subject = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ generated_from_issue = Field(attr="generated_from_issue_id")
+ external_reference = Field()
+ tribe_gig = Field()
+ version = Field()
+ watchers = Field()
+ is_blocked = Field()
+ blocked_note = Field()
+ total_points = MethodField()
+ comment = MethodField()
+ origin_issue = OriginIssueSerializer(attr="generated_from_issue")
+ epics = MethodField()
+ epic_order = MethodField()
+ tasks = MethodField()
+ def get_epic_order(self, obj):
+ include_epic_order = getattr(obj, "include_epic_order", False)
-class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- tags = TagsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
- points = RolePointsField(source="role_points", required=False)
- total_points = serializers.SerializerMethodField("get_total_points")
- comment = serializers.SerializerMethodField("get_comment")
- milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
- milestone_name = serializers.SerializerMethodField("get_milestone_name")
- origin_issue = serializers.SerializerMethodField("get_origin_issue")
- blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
- description_html = serializers.SerializerMethodField("get_description_html")
- status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True)
- assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
- owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
- tribe_gig = PickledObjectField(required=False)
+ if include_epic_order:
+ assert hasattr(obj, "epic_order"), "instance must have a epic_order attribute"
- class Meta:
- model = models.UserStory
- depth = 0
- read_only_fields = ('created_date', 'modified_date', 'owner')
+ if not include_epic_order or obj.epic_order is None:
+ return None
+
+ return obj.epic_order
+
+ def get_epics(self, obj):
+ assert hasattr(obj, "epics_attr"), "instance must have a epics_attr attribute"
+ return obj.epics_attr
+
+ def get_milestone_slug(self, obj):
+ return obj.milestone.slug if obj.milestone else None
+
+ def get_milestone_name(self, obj):
+ return obj.milestone.name if obj.milestone else None
def get_total_points(self, obj):
- return obj.get_total_points()
+ assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
+ return obj.total_points_attr
+
+ def get_points(self, obj):
+ assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute"
+ if obj.role_points_attr is None:
+ return {}
+
+ return obj.role_points_attr
+
+ def get_comment(self, obj):
+ return ""
+
+ def get_tasks(self, obj):
+ include_tasks = getattr(obj, "include_tasks", False)
+
+ if include_tasks:
+ assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute"
+
+ if not include_tasks or obj.tasks_attr is None:
+ return []
+
+ return obj.tasks_attr
+
+
+class UserStorySerializer(UserStoryListSerializer):
+ comment = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work
return ""
- def get_milestone_slug(self, obj):
- if obj.milestone:
- return obj.milestone.slug
- else:
- return None
-
- def get_milestone_name(self, obj):
- if obj.milestone:
- return obj.milestone.name
- else:
- return None
-
- def get_origin_issue(self, obj):
- if obj.generated_from_issue:
- return {
- "id": obj.generated_from_issue.id,
- "ref": obj.generated_from_issue.ref,
- "subject": obj.generated_from_issue.subject,
- }
- return None
-
def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note)
@@ -105,41 +145,5 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, Editab
return mdrender(obj.project, obj.description)
-class UserStoryListSerializer(UserStorySerializer):
- class Meta:
- model = models.UserStory
- depth = 0
- read_only_fields = ('created_date', 'modified_date')
- exclude=("description", "description_html")
-
-
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
- def serialize_neighbor(self, neighbor):
- if neighbor:
- return NeighborUserStorySerializer(neighbor).data
- return None
-
-
-class NeighborUserStorySerializer(serializers.ModelSerializer):
- class Meta:
- model = models.UserStory
- fields = ("id", "ref", "subject")
- depth = 0
-
-
-class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- status_id = serializers.IntegerField(required=False)
- bulk_stories = serializers.CharField()
-
-
-## Order bulk serializers
-
-class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
- us_id = serializers.IntegerField()
- order = serializers.IntegerField()
-
-
-class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_stories = _UserStoryOrderBulkSerializer(many=True)
+ pass
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index f81ae8e5..2a381eb0 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -28,10 +28,9 @@ from django.utils.translation import ugettext as _
from taiga.base.utils import db, text
from taiga.projects.history.services import take_snapshot
-from taiga.projects.userstories.apps import (
- connect_userstories_signals,
- disconnect_userstories_signals)
-
+from taiga.projects.services import apply_order_updates
+from taiga.projects.userstories.apps import connect_userstories_signals
+from taiga.projects.userstories.apps import disconnect_userstories_signals
from taiga.events import events
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.notifications.utils import attach_watchers_to_queryset
@@ -39,6 +38,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset
from . import models
+#####################################################
+# Bulk actions
+#####################################################
+
def get_userstories_from_bulk(bulk_data, **additional_fields):
"""Convert `bulk_data` into a list of user stories.
@@ -72,28 +75,64 @@ def create_userstories_in_bulk(bulk_data, callback=None, precall=None, **additio
return userstories
-def update_userstories_order_in_bulk(bulk_data:list, field:str, project:object):
+def update_userstories_order_in_bulk(bulk_data: list, field: str, project: object,
+ status: object=None, milestone: object=None):
"""
- Update the order of some user stories.
- `bulk_data` should be a list of tuples with the following format:
+ Updates the order of the userstories specified adding the extra updates needed
+ to keep consistency.
+ `bulk_data` should be a list of dicts with the following format:
+ `field` is the order field used
- [(, {: , ...}), ...]
+ [{'us_id': , 'order': }, ...]
"""
- user_story_ids = []
- new_order_values = []
- for us_data in bulk_data:
- user_story_ids.append(us_data["us_id"])
- new_order_values.append({field: us_data["order"]})
+ user_stories = project.user_stories.all()
+ if status is not None:
+ user_stories = user_stories.filter(status=status)
+ if milestone is not None:
+ user_stories = user_stories.filter(milestone=milestone)
+ us_orders = {us.id: getattr(us, field) for us in user_stories}
+ new_us_orders = {e["us_id"]: e["order"] for e in bulk_data}
+ apply_order_updates(us_orders, new_us_orders)
+
+ user_story_ids = us_orders.keys()
events.emit_event_for_ids(ids=user_story_ids,
content_type="userstories.userstory",
projectid=project.pk)
+ db.update_attr_in_bulk_for_ids(us_orders, field, models.UserStory)
+ return us_orders
- db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory)
+
+def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object):
+ """
+ Update the milestone and the milestone order of some user stories adding the
+ extra orders needed to keep consistency.
+ `bulk_data` should be a list of dicts with the following format:
+ [{'us_id': , 'order': }, ...]
+ """
+ user_stories = milestone.user_stories.all()
+ us_orders = {us.id: getattr(us, "sprint_order") for us in user_stories}
+ new_us_orders = {}
+ for e in bulk_data:
+ new_us_orders[e["us_id"]] = e["order"]
+ # The base orders where we apply the new orders must containg all the values
+ us_orders[e["us_id"]] = e["order"]
+
+ apply_order_updates(us_orders, new_us_orders)
+
+ us_milestones = {e["us_id"]: milestone.id for e in bulk_data}
+ user_story_ids = us_milestones.keys()
+
+ events.emit_event_for_ids(ids=user_story_ids,
+ content_type="userstories.userstory",
+ projectid=milestone.project.pk)
+
+ db.update_attr_in_bulk_for_ids(us_milestones, "milestone_id", model=models.UserStory)
+ db.update_attr_in_bulk_for_ids(us_orders, "sprint_order", models.UserStory)
+ return us_orders
def snapshot_userstories_in_bulk(bulk_data, user):
- user_story_ids = []
for us_data in bulk_data:
try:
us = models.UserStory.objects.get(pk=us_data['us_id'])
@@ -102,6 +141,10 @@ def snapshot_userstories_in_bulk(bulk_data, user):
pass
+#####################################################
+# Open/Close calcs
+#####################################################
+
def calculate_userstory_is_closed(user_story):
if user_story.status is None:
return False
@@ -129,7 +172,11 @@ def open_userstory(us):
us.save(update_fields=["is_closed", "finish_date"])
-def userstories_to_csv(project,queryset):
+#####################################################
+# CSV
+#####################################################
+
+def userstories_to_csv(project, queryset):
csv_data = io.StringIO()
fieldnames = ["ref", "subject", "description", "sprint", "sprint_estimated_start",
"sprint_estimated_finish", "owner", "owner_full_name", "assigned_to",
@@ -145,7 +192,7 @@ def userstories_to_csv(project,queryset):
"created_date", "modified_date", "finish_date",
"client_requirement", "team_requirement", "attachments",
"generated_from_issue", "external_reference", "tasks",
- "tags","watchers", "voters"]
+ "tags", "watchers", "voters"]
custom_attrs = project.userstorycustomattributes.all()
for custom_attr in custom_attrs:
@@ -197,7 +244,7 @@ def userstories_to_csv(project,queryset):
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
"tags": ",".join(us.tags or []),
"watchers": us.watchers,
- "voters": us.total_voters,
+ "voters": us.total_voters
}
us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()}
@@ -215,6 +262,10 @@ def userstories_to_csv(project,queryset):
return csv_data
+#####################################################
+# Api filter data
+#####################################################
+
def _get_userstories_statuses(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
@@ -222,18 +273,33 @@ def _get_userstories_statuses(project, queryset):
where_params = queryset_where_tuple[1]
extra_sql = """
- SELECT "projects_userstorystatus"."id",
- "projects_userstorystatus"."name",
- "projects_userstorystatus"."color",
- "projects_userstorystatus"."order",
- (SELECT count(*)
- FROM "userstories_userstory"
- INNER JOIN "projects_project" ON
- ("userstories_userstory"."project_id" = "projects_project"."id")
- WHERE {where} AND "userstories_userstory"."status_id" = "projects_userstorystatus"."id")
- FROM "projects_userstorystatus"
- WHERE "projects_userstorystatus"."project_id" = %s
- ORDER BY "projects_userstorystatus"."order";
+ WITH "us_counters" AS (
+ SELECT DISTINCT "userstories_userstory"."status_id" "status_id",
+ "userstories_userstory"."id" "us_id"
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ LEFT OUTER JOIN "epics_relateduserstory"
+ ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id"
+ WHERE {where}
+ ),
+ "counters" AS (
+ SELECT "status_id",
+ COUNT("status_id") "count"
+ FROM "us_counters"
+ GROUP BY "status_id"
+ )
+
+ SELECT "projects_userstorystatus"."id",
+ "projects_userstorystatus"."name",
+ "projects_userstorystatus"."color",
+ "projects_userstorystatus"."order",
+ COALESCE("counters"."count", 0)
+ FROM "projects_userstorystatus"
+ LEFT OUTER JOIN "counters"
+ ON "counters"."status_id" = "projects_userstorystatus"."id"
+ WHERE "projects_userstorystatus"."project_id" = %s
+ ORDER BY "projects_userstorystatus"."order";
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -259,31 +325,49 @@ def _get_userstories_assigned_to(project, queryset):
where_params = queryset_where_tuple[1]
extra_sql = """
- WITH counters AS (
- SELECT assigned_to_id, count(assigned_to_id) count
- FROM "userstories_userstory"
- INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
- WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NOT NULL
- GROUP BY assigned_to_id
- )
+ WITH "us_counters" AS (
+ SELECT DISTINCT "userstories_userstory"."assigned_to_id" "assigned_to_id",
+ "userstories_userstory"."id" "us_id"
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ LEFT OUTER JOIN "epics_relateduserstory"
+ ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id"
+ WHERE {where}
+ ),
- SELECT "projects_membership"."user_id" user_id,
- "users_user"."full_name",
- "users_user"."username",
- COALESCE("counters".count, 0) count
- FROM projects_membership
- LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
- INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
+ "counters" AS (
+ SELECT "assigned_to_id",
+ COUNT("assigned_to_id")
+ FROM "us_counters"
+ GROUP BY "assigned_to_id"
+ )
+
+ SELECT "projects_membership"."user_id" "user_id",
+ "users_user"."full_name" "full_name",
+ "users_user"."username" "username",
+ COALESCE("counters".count, 0) "count"
+ FROM "projects_membership"
+ LEFT OUTER JOIN "counters"
+ ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
+ INNER JOIN "users_user"
+ ON ("projects_membership"."user_id" = "users_user"."id")
WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
-- unassigned userstories
UNION
- SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
+ SELECT NULL "user_id",
+ NULL "full_name",
+ NULL "username",
+ count(coalesce("assigned_to_id", -1)) "count"
FROM "userstories_userstory"
- INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ LEFT OUTER JOIN "epics_relateduserstory"
+ ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id")
WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL
- GROUP BY assigned_to_id
+ GROUP BY "assigned_to_id"
""".format(where=where)
with closing(connection.cursor()) as cursor:
@@ -320,32 +404,45 @@ def _get_userstories_owners(project, queryset):
where_params = queryset_where_tuple[1]
extra_sql = """
- WITH counters AS (
- SELECT "userstories_userstory"."owner_id" owner_id, count(coalesce("userstories_userstory"."owner_id", -1)) count
- FROM "userstories_userstory"
- INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id")
- WHERE {where}
- GROUP BY "userstories_userstory"."owner_id"
- )
+ WITH "us_counters" AS(
+ SELECT DISTINCT "userstories_userstory"."owner_id" "owner_id",
+ "userstories_userstory"."id" "us_id"
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ LEFT OUTER JOIN "epics_relateduserstory"
+ ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id")
+ WHERE {where}
+ ),
- SELECT "projects_membership"."user_id" id,
+ "counters" AS (
+ SELECT "owner_id",
+ COUNT("owner_id")
+ FROM "us_counters"
+ GROUP BY "owner_id"
+ )
+
+ SELECT "projects_membership"."user_id" "user_id",
"users_user"."full_name",
"users_user"."username",
- COALESCE("counters".count, 0) count
- FROM projects_membership
- LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
- INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
- WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL)
+ COALESCE("counters".count, 0) "count"
+ FROM "projects_membership"
+ LEFT OUTER JOIN "counters"
+ ON ("projects_membership"."user_id" = "counters"."owner_id")
+ INNER JOIN "users_user"
+ ON ("projects_membership"."user_id" = "users_user"."id")
+ WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
-- System users
- UNION
+ UNION
- SELECT "users_user"."id" user_id,
- "users_user"."full_name" full_name,
- "users_user"."username" username,
- COALESCE("counters".count, 0) count
- FROM users_user
- LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
+ SELECT "users_user"."id" "user_id",
+ "users_user"."full_name" "full_name",
+ "users_user"."username" "username",
+ COALESCE("counters"."count", 0) "count"
+ FROM "users_user"
+ LEFT OUTER JOIN "counters"
+ ON ("users_user"."id" = "counters"."owner_id")
WHERE ("users_user"."is_system" IS TRUE)
""".format(where=where)
@@ -364,16 +461,127 @@ def _get_userstories_owners(project, queryset):
return sorted(result, key=itemgetter("full_name"))
-def _get_userstories_tags(queryset):
- tags = []
- for t_list in queryset.values_list("tags", flat=True):
- if t_list is None:
- continue
- tags += list(t_list)
+def _get_userstories_tags(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
- tags = [{"name":e, "count":tags.count(e)} for e in set(tags)]
+ extra_sql = """
+ WITH "userstories_tags" AS (
+ SELECT "tag",
+ COUNT("tag") "counter"
+ FROM (
+ SELECT DISTINCT "userstories_userstory"."id" "us_id",
+ UNNEST("userstories_userstory"."tags") "tag"
+ FROM "userstories_userstory"
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ LEFT OUTER JOIN "epics_relateduserstory"
+ ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id")
+ WHERE {where}
+ ) "tags"
+ GROUP BY "tag"),
- return sorted(tags, key=itemgetter("name"))
+ "project_tags" AS (
+ SELECT reduce_dim("tags_colors") "tag_color"
+ FROM "projects_project"
+ WHERE "id"=%s)
+
+ SELECT "tag_color"[1] "tag",
+ "tag_color"[2] "color",
+ COALESCE("userstories_tags"."counter", 0) "counter"
+ FROM "project_tags"
+LEFT OUTER JOIN "userstories_tags"
+ ON "project_tags"."tag_color"[1] = "userstories_tags"."tag"
+ ORDER BY "tag"
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for name, color, count in rows:
+ result.append({
+ "name": name,
+ "color": color,
+ "count": count,
+ })
+ return sorted(result, key=itemgetter("name"))
+
+
+def _get_userstories_epics(project, queryset):
+ compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
+ queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
+ where = queryset_where_tuple[0]
+ where_params = queryset_where_tuple[1]
+ extra_sql = """
+ WITH "counters" AS (
+ SELECT "epics_relateduserstory"."epic_id" AS "epic_id",
+ count("epics_relateduserstory"."id") AS "counter"
+ FROM "epics_relateduserstory"
+ INNER JOIN "userstories_userstory"
+ ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id")
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ WHERE {where}
+ GROUP BY "epics_relateduserstory"."epic_id"
+ )
+
+ -- User stories with no epics (return results only if there are userstories)
+ SELECT NULL AS "id",
+ NULL AS "ref",
+ NULL AS "subject",
+ 0 AS "order",
+ count(COALESCE("epics_relateduserstory"."epic_id", -1)) AS "counter"
+ FROM "userstories_userstory"
+ LEFT OUTER JOIN "epics_relateduserstory"
+ ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id")
+ INNER JOIN "projects_project"
+ ON ("userstories_userstory"."project_id" = "projects_project"."id")
+ WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL
+ GROUP BY "epics_relateduserstory"."epic_id"
+
+ UNION
+
+ SELECT "epics_epic"."id" AS "id",
+ "epics_epic"."ref" AS "ref",
+ "epics_epic"."subject" AS "subject",
+ "epics_epic"."epics_order" AS "order",
+ COALESCE("counters"."counter", 0) AS "counter"
+ FROM "epics_epic"
+ LEFT OUTER JOIN "counters"
+ ON ("counters"."epic_id" = "epics_epic"."id")
+ WHERE "epics_epic"."project_id" = %s
+ """.format(where=where)
+
+ with closing(connection.cursor()) as cursor:
+ cursor.execute(extra_sql, where_params + where_params + [project.id])
+ rows = cursor.fetchall()
+
+ result = []
+ for id, ref, subject, order, count in rows:
+ result.append({
+ "id": id,
+ "ref": ref,
+ "subject": subject,
+ "order": order,
+ "count": count,
+ })
+
+ result = sorted(result, key=lambda k: (k["order"], k["id"] or 0))
+
+ # Add row when there is no user stories with no epics
+ if result == [] or result[0]["id"] is not None:
+ result.insert(0, {
+ "id": None,
+ "ref": None,
+ "subject": None,
+ "order": 0,
+ "count": 0,
+ })
+ return result
def get_userstories_filters_data(project, querysets):
@@ -385,7 +593,8 @@ def get_userstories_filters_data(project, querysets):
("statuses", _get_userstories_statuses(project, querysets["statuses"])),
("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])),
("owners", _get_userstories_owners(project, querysets["owners"])),
- ("tags", _get_userstories_tags(querysets["tags"])),
+ ("tags", _get_userstories_tags(project, querysets["tags"])),
+ ("epics", _get_userstories_epics(project, querysets["epics"])),
])
return data
diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py
index 11638595..fc1fdacc 100644
--- a/taiga/projects/userstories/signals.py
+++ b/taiga/projects/userstories/signals.py
@@ -59,7 +59,7 @@ def update_role_points_when_create_or_edit_us(sender, instance, **kwargs):
def update_milestone_of_tasks_when_edit_us(sender, instance, created, **kwargs):
if not created:
- instance.tasks.update(milestone=instance.milestone)
+ instance.tasks.exclude(milestone=instance.milestone).update(milestone=instance.milestone)
for task in instance.tasks.all():
take_snapshot(task)
diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py
new file mode 100644
index 00000000..57e4ecd3
--- /dev/null
+++ b/taiga/projects/userstories/utils.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.attachments.utils import attach_basic_attachments
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_total_points(queryset, as_field="total_points_attr"):
+ """Attach total of point values to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT SUM(projects_points.value)
+ FROM userstories_rolepoints
+ INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
+ WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_role_points(queryset, as_field="role_points_attr"):
+ """Attach role point as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the role points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT FORMAT('{{%%s}}',
+ STRING_AGG(format(
+ '"%%s":%%s',
+ TO_JSON(userstories_rolepoints.role_id),
+ TO_JSON(userstories_rolepoints.points_id)
+ ), ',')
+ )::json
+ FROM userstories_rolepoints
+ WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_tasks(queryset, as_field="tasks_attr"):
+ """Attach tasks as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach tasks as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ tasks_task.id,
+ tasks_task.ref,
+ tasks_task.subject,
+ tasks_task.status_id,
+ tasks_task.is_blocked,
+ tasks_task.is_iocaine,
+ projects_taskstatus.is_closed
+ FROM tasks_task
+ INNER JOIN projects_taskstatus on projects_taskstatus.id = tasks_task.status_id
+ WHERE user_story_id = {tbl}.id
+ ORDER BY tasks_task.us_order, tasks_task.ref
+ ) t
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_epics(queryset, as_field="epics_attr"):
+ """Attach epics as json column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param as_field: Attach the epics as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM (SELECT "epics_epic"."id" AS "id",
+ "epics_epic"."ref" AS "ref",
+ "epics_epic"."subject" AS "subject",
+ "epics_epic"."color" AS "color",
+ (SELECT row_to_json(p)
+ FROM (SELECT "projects_project"."id" AS "id",
+ "projects_project"."name" AS "name",
+ "projects_project"."slug" AS "slug"
+ ) p
+ ) AS "project"
+ FROM "epics_relateduserstory"
+ INNER JOIN "epics_epic" ON "epics_epic"."id" = "epics_relateduserstory"."epic_id"
+ INNER JOIN "projects_project" ON "projects_project"."id" = "epics_epic"."project_id"
+ WHERE "epics_relateduserstory"."user_story_id" = {tbl}.id
+ ORDER BY "projects_project"."name", "epics_epic"."ref") t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_epic_order(queryset, epic_id, as_field="epic_order"):
+ """Attach epic_order column to each object of the queryset.
+
+ :param queryset: A Django user stories queryset object.
+ :param epic_id: Order related to this epic.
+ :param as_field: Attach order as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+
+ model = queryset.model
+ sql = """SELECT "epics_relateduserstory"."order" AS "epic_order"
+ FROM "epics_relateduserstory"
+ WHERE "epics_relateduserstory"."user_story_id" = {tbl}.id and
+ "epics_relateduserstory"."epic_id" = {epic_id}"""
+
+ sql = sql.format(tbl=model._meta.db_table, epic_id=epic_id)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False, epic_id=None):
+ queryset = attach_total_points(queryset)
+ queryset = attach_role_points(queryset)
+ queryset = attach_epics(queryset)
+
+ if include_attachments:
+ queryset = attach_basic_attachments(queryset)
+ queryset = queryset.extra(select={"include_attachments": "True"})
+
+ if include_tasks:
+ queryset = attach_tasks(queryset)
+ queryset = queryset.extra(select={"include_tasks": "True"})
+
+ if epic_id is not None:
+ queryset = attach_epic_order(queryset, epic_id)
+ queryset = queryset.extra(select={"include_epic_order": "True"})
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py
index 5ad5e7f4..e20a704e 100644
--- a/taiga/projects/userstories/validators.py
+++ b/taiga/projects/userstories/validators.py
@@ -19,14 +19,153 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import PgArrayField
+from taiga.base.fields import PickledObjectField
+from taiga.projects.milestones.models import Milestone
+from taiga.projects.models import UserStoryStatus
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.userstories.models import UserStory
+from taiga.projects.validators import ProjectExistsValidator
from . import models
+import json
+
class UserStoryExistsValidator:
def validate_us_id(self, attrs, source):
value = attrs[source]
if not models.UserStory.objects.filter(pk=value).exists():
msg = _("There's no user story with that id")
- raise serializers.ValidationError(msg)
+ raise ValidationError(msg)
+ return attrs
+
+
+class RolePointsField(serializers.WritableField):
+ def to_native(self, obj):
+ return {str(o.role.id): o.points.id for o in obj.all()}
+
+ def from_native(self, obj):
+ if isinstance(obj, dict):
+ return obj
+ return json.loads(obj)
+
+
+class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+ points = RolePointsField(source="role_points", required=False)
+ tribe_gig = PickledObjectField(required=False)
+
+ class Meta:
+ model = models.UserStory
+ depth = 0
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+
+class UserStoriesBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ bulk_stories = serializers.CharField()
+
+ def validate_status_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+
+ if not UserStoryStatus.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid user story status id. The status must belong to "
+ "the same project."))
+
+ return attrs
+
+
+# Order bulk validators
+
+class _UserStoryOrderBulkValidator(validators.Validator):
+ us_id = serializers.IntegerField()
+ order = serializers.IntegerField()
+
+
+class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ milestone_id = serializers.IntegerField(required=False)
+ bulk_stories = _UserStoryOrderBulkValidator(many=True)
+
+ def validate_status_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+
+ if not UserStoryStatus.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid user story status id. The status must belong "
+ "to the same project."))
+
+ return attrs
+
+ def validate_milestone_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+
+ if not Milestone.objects.filter(**filters).exists():
+ raise ValidationError(_("Invalid milestone id. The milistone must belong to the "
+ "same project."))
+
+ return attrs
+
+ def validate_bulk_stories(self, attrs, source):
+ filters = {"project__id": attrs["project_id"]}
+ if "status_id" in attrs:
+ filters["status__id"] = attrs["status_id"]
+ if "milestone_id" in attrs:
+ filters["milestone__id"] = attrs["milestone_id"]
+
+ filters["id__in"] = [us["us_id"] for us in attrs[source]]
+
+ if models.UserStory.objects.filter(**filters).count() != len(filters["id__in"]):
+ raise ValidationError(_("Invalid user story ids. All stories must belong to the same project "
+ "and, if it exists, to the same status and milestone."))
+
+ return attrs
+
+
+# Milestone bulk validators
+
+class _UserStoryMilestoneBulkValidator(validators.Validator):
+ us_id = serializers.IntegerField()
+ order = serializers.IntegerField()
+
+
+class UpdateMilestoneBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ milestone_id = serializers.IntegerField()
+ bulk_stories = _UserStoryMilestoneBulkValidator(many=True)
+
+ def validate_milestone_id(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id": attrs[source]
+ }
+ if not Milestone.objects.filter(**filters).exists():
+ raise ValidationError(_("The milestone isn't valid for the project"))
+ return attrs
+
+ def validate_bulk_stories(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id__in": [us["us_id"] for us in attrs[source]]
+ }
+
+ if UserStory.objects.filter(**filters).count() != len(filters["id__in"]):
+ raise ValidationError(_("All the user stories must be from the same project"))
+
return attrs
diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py
new file mode 100644
index 00000000..d56a96c9
--- /dev/null
+++ b/taiga/projects/utils.py
@@ -0,0 +1,505 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+def attach_members(queryset, as_field="members_attr"):
+ """Attach a json members representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the members as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ users_user.id,
+ users_user.username,
+ users_user.full_name,
+ users_user.email,
+ concat(full_name, username) complete_user_name,
+ users_user.color,
+ users_user.photo,
+ users_user.is_active,
+ users_role.id "role",
+ users_role.name role_name
+
+ FROM projects_membership
+ LEFT JOIN users_user ON projects_membership.user_id = users_user.id
+ LEFT JOIN users_role ON users_role.id = projects_membership.role_id
+ WHERE projects_membership.project_id = {tbl}.id
+ ORDER BY complete_user_name) t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_milestones(queryset, as_field="milestones_attr"):
+ """Attach a json milestons representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the milestones as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ milestones_milestone.id,
+ milestones_milestone.slug,
+ milestones_milestone.name,
+ milestones_milestone.closed
+ FROM milestones_milestone
+ WHERE milestones_milestone.project_id = {tbl}.id
+ ORDER BY estimated_start) t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_closed_milestones(queryset, as_field="closed_milestones_attr"):
+ """Attach a closed milestones counter to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the counter as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT COUNT(milestones_milestone.id)
+ FROM milestones_milestone
+ WHERE
+ milestones_milestone.project_id = {tbl}.id AND
+ milestones_milestone.closed = True
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_notify_policies(queryset, as_field="notify_policies_attr"):
+ """Attach a json notification policies representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the notification policies as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(notifications_notifypolicy))
+ FROM notifications_notifypolicy
+ WHERE
+ notifications_notifypolicy.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_epic_statuses(queryset, as_field="epic_statuses_attr"):
+ """Attach a json epic statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the epic statuses as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_epicstatus))
+ FROM projects_epicstatus
+ WHERE
+ projects_epicstatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_userstory_statuses(queryset, as_field="userstory_statuses_attr"):
+ """Attach a json userstory statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the userstory statuses as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_userstorystatus))
+ FROM projects_userstorystatus
+ WHERE
+ projects_userstorystatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_points(queryset, as_field="points_attr"):
+ """Attach a json points representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the points as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_points))
+ FROM projects_points
+ WHERE
+ projects_points.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_task_statuses(queryset, as_field="task_statuses_attr"):
+ """Attach a json task statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the task statuses as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_taskstatus))
+ FROM projects_taskstatus
+ WHERE
+ projects_taskstatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_issue_statuses(queryset, as_field="issue_statuses_attr"):
+ """Attach a json issue statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the statuses as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_issuestatus))
+ FROM projects_issuestatus
+ WHERE
+ projects_issuestatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_issue_types(queryset, as_field="issue_types_attr"):
+ """Attach a json issue types representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the types as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_issuetype))
+ FROM projects_issuetype
+ WHERE
+ projects_issuetype.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_priorities(queryset, as_field="priorities_attr"):
+ """Attach a json priorities representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the priorities as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_priority))
+ FROM projects_priority
+ WHERE
+ projects_priority.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_severities(queryset, as_field="severities_attr"):
+ """Attach a json severities representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the severities as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(projects_severity))
+ FROM projects_severity
+ WHERE
+ projects_severity.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_epic_custom_attributes(queryset, as_field="epic_custom_attributes_attr"):
+ """Attach a json epic custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the epic custom attributes as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(custom_attributes_epiccustomattribute))
+ FROM custom_attributes_epiccustomattribute
+ WHERE
+ custom_attributes_epiccustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_userstory_custom_attributes(queryset, as_field="userstory_custom_attributes_attr"):
+ """Attach a json userstory custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the userstory custom attributes as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(custom_attributes_userstorycustomattribute))
+ FROM custom_attributes_userstorycustomattribute
+ WHERE
+ custom_attributes_userstorycustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_task_custom_attributes(queryset, as_field="task_custom_attributes_attr"):
+ """Attach a json task custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the task custom attributes as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(custom_attributes_taskcustomattribute))
+ FROM custom_attributes_taskcustomattribute
+ WHERE
+ custom_attributes_taskcustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_issue_custom_attributes(queryset, as_field="issue_custom_attributes_attr"):
+ """Attach a json issue custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the issue custom attributes as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(custom_attributes_issuecustomattribute))
+ FROM custom_attributes_issuecustomattribute
+ WHERE
+ custom_attributes_issuecustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_roles(queryset, as_field="roles_attr"):
+ """Attach a json roles representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the roles as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(users_role))
+ FROM users_role
+ WHERE
+ users_role.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_is_fan(queryset, user, as_field="is_fan_attr"):
+ """Attach a is fan boolean to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the boolean as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT false"""
+ else:
+ sql = """SELECT COUNT(likes_like.id) > 0
+ FROM likes_like
+ INNER JOIN django_content_type
+ ON likes_like.content_type_id = django_content_type.id
+ WHERE
+ django_content_type.model = 'project' AND
+ django_content_type.app_label = 'projects' AND
+ likes_like.user_id = {user_id} AND
+ likes_like.object_id = {tbl}.id"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_my_role_permissions(queryset, user, as_field="my_role_permissions_attr"):
+ """Attach a permission array to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the permissions as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT '{}'"""
+ else:
+ sql = """SELECT users_role.permissions
+ FROM projects_membership
+ LEFT JOIN users_user ON projects_membership.user_id = users_user.id
+ LEFT JOIN users_role ON users_role.id = projects_membership.role_id
+ WHERE
+ projects_membership.project_id = {tbl}.id AND
+ users_user.id = {user_id}"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_private_projects_same_owner(queryset, user, as_field="private_projects_same_owner_attr"):
+ """Attach a private projects counter to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the counter as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT 0"""
+ else:
+ sql = """SELECT COUNT(id)
+ FROM projects_project p_aux
+ WHERE
+ p_aux.is_private = True AND
+ p_aux.owner_id = {tbl}.owner_id"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_public_projects_same_owner(queryset, user, as_field="public_projects_same_owner_attr"):
+ """Attach a public projects counter to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the counter as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT 0"""
+ else:
+ sql = """SELECT COUNT(id)
+ FROM projects_project p_aux
+ WHERE
+ p_aux.is_private = False AND
+ p_aux.owner_id = {tbl}.owner_id"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None):
+ queryset = attach_members(queryset)
+ queryset = attach_closed_milestones(queryset)
+ queryset = attach_notify_policies(queryset)
+ queryset = attach_epic_statuses(queryset)
+ queryset = attach_userstory_statuses(queryset)
+ queryset = attach_points(queryset)
+ queryset = attach_task_statuses(queryset)
+ queryset = attach_issue_statuses(queryset)
+ queryset = attach_issue_types(queryset)
+ queryset = attach_priorities(queryset)
+ queryset = attach_severities(queryset)
+ queryset = attach_epic_custom_attributes(queryset)
+ queryset = attach_userstory_custom_attributes(queryset)
+ queryset = attach_task_custom_attributes(queryset)
+ queryset = attach_issue_custom_attributes(queryset)
+ queryset = attach_roles(queryset)
+ queryset = attach_is_fan(queryset, user)
+ queryset = attach_my_role_permissions(queryset, user)
+ queryset = attach_private_projects_same_owner(queryset, user)
+ queryset = attach_public_projects_same_owner(queryset, user)
+ queryset = attach_milestones(queryset)
+
+ return queryset
diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py
index 05866b66..54a43178 100644
--- a/taiga/projects/validators.py
+++ b/taiga/projects/validators.py
@@ -16,11 +16,42 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.db.models import Q
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import JsonField
+from taiga.base.fields import PgArrayField
+from taiga.users.models import Role
+
+from .tagging.fields import TagsField
from . import models
+from . import services
+
+
+class DuplicatedNameInProjectValidator:
+ def validate_name(self, attrs, source):
+ """
+ Check the points name is not duplicated in the project on creation
+ """
+ model = self.opts.model
+ qs = None
+ # If the object exists:
+ if self.object and attrs.get(source, None):
+ qs = model.objects.filter(
+ project=self.object.project,
+ name=attrs[source]).exclude(id=self.object.id)
+
+ if not self.object and attrs.get("project", None) and attrs.get(source, None):
+ qs = model.objects.filter(project=attrs["project"], name=attrs[source])
+
+ if qs and qs.exists():
+ raise ValidationError(_("Name duplicated for the project"))
+
+ return attrs
class ProjectExistsValidator:
@@ -28,23 +59,188 @@ class ProjectExistsValidator:
value = attrs[source]
if not models.Project.objects.filter(pk=value).exists():
msg = _("There's no project with that id")
- raise serializers.ValidationError(msg)
+ raise ValidationError(msg)
return attrs
-class UserStoryStatusExistsValidator:
- def validate_status_id(self, attrs, source):
- value = attrs[source]
- if not models.UserStoryStatus.objects.filter(pk=value).exists():
- msg = _("There's no user story status with that id")
- raise serializers.ValidationError(msg)
+######################################################
+# Custom values for selectors
+######################################################
+
+class EpicStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.EpicStatus
+
+
+class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.UserStoryStatus
+
+
+class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Points
+
+
+class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.TaskStatus
+
+
+class SeverityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Severity
+
+
+class PriorityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Priority
+
+
+class IssueStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.IssueStatus
+
+
+class IssueTypeValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.IssueType
+
+
+######################################################
+# Members
+######################################################
+
+class MembershipValidator(validators.ModelValidator):
+ email = serializers.EmailField(required=True)
+
+ class Meta:
+ model = models.Membership
+ # IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
+ # with this info (excluding here user_email and email)
+ read_only_fields = ("user",)
+ exclude = ("token", "email")
+
+ def validate_email(self, attrs, source):
+ project = attrs.get("project", None)
+ if project is None:
+ project = self.object.project
+
+ email = attrs[source]
+
+ qs = models.Membership.objects.all()
+
+ # If self.object is not None, the serializer is in update
+ # mode, and for it, it should exclude self.
+ if self.object:
+ qs = qs.exclude(pk=self.object.pk)
+
+ qs = qs.filter(Q(project_id=project.id, user__email=email) |
+ Q(project_id=project.id, email=email))
+
+ if qs.count() > 0:
+ raise ValidationError(_("Email address is already taken"))
+
+ return attrs
+
+ def validate_role(self, attrs, source):
+ project = attrs.get("project", None)
+ if project is None:
+ project = self.object.project
+
+ role = attrs[source]
+
+ if project.roles.filter(id=role.id).count() == 0:
+ raise ValidationError(_("Invalid role for the project"))
+
+ return attrs
+
+ def validate_is_admin(self, attrs, source):
+ project = attrs.get("project", None)
+ if project is None:
+ project = self.object.project
+
+ if (self.object and self.object.user):
+ if self.object.user.id == project.owner_id and not attrs[source]:
+ raise ValidationError(_("The project owner must be admin."))
+
+ if not services.project_has_valid_admins(project, exclude_user=self.object.user):
+ raise ValidationError(
+ _("At least one user must be an active admin for this project.")
+ )
+
return attrs
-class TaskStatusExistsValidator:
- def validate_status_id(self, attrs, source):
- value = attrs[source]
- if not models.TaskStatus.objects.filter(pk=value).exists():
- msg = _("There's no task status with that id")
- raise serializers.ValidationError(msg)
+class MembershipAdminValidator(MembershipValidator):
+ class Meta:
+ model = models.Membership
+ # IMPORTANT: Maintain the MembershipSerializer Meta up to date
+ # with this info (excluding there user_email and email)
+ read_only_fields = ("user",)
+ exclude = ("token",)
+
+
+class _MemberBulkValidator(validators.Validator):
+ email = serializers.EmailField()
+ role_id = serializers.IntegerField()
+
+
+class MembersBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_memberships = _MemberBulkValidator(many=True)
+ invitation_extra_text = serializers.CharField(required=False, max_length=255)
+
+ def validate_bulk_memberships(self, attrs, source):
+ filters = {
+ "project__id": attrs["project_id"],
+ "id__in": [r["role_id"] for r in attrs["bulk_memberships"]]
+ }
+
+ if Role.objects.filter(**filters).count() != len(set(filters["id__in"])):
+ raise ValidationError(_("Invalid role ids. All roles must belong to the same project."))
+
return attrs
+
+
+######################################################
+# Projects
+######################################################
+
+class ProjectValidator(validators.ModelValidator):
+ anon_permissions = PgArrayField(required=False)
+ public_permissions = PgArrayField(required=False)
+ tags = TagsField(default=[], required=False)
+
+ class Meta:
+ model = models.Project
+ read_only_fields = ("created_date", "modified_date", "slug", "blocked_code", "owner")
+
+
+######################################################
+# Project Templates
+######################################################
+
+class ProjectTemplateValidator(validators.ModelValidator):
+ default_options = JsonField(required=False, label=_("Default options"))
+ us_statuses = JsonField(required=False, label=_("User story's statuses"))
+ points = JsonField(required=False, label=_("Points"))
+ task_statuses = JsonField(required=False, label=_("Task's statuses"))
+ issue_statuses = JsonField(required=False, label=_("Issue's statuses"))
+ issue_types = JsonField(required=False, label=_("Issue's types"))
+ priorities = JsonField(required=False, label=_("Priorities"))
+ severities = JsonField(required=False, label=_("Severities"))
+ roles = JsonField(required=False, label=_("Roles"))
+
+ class Meta:
+ model = models.ProjectTemplate
+ read_only_fields = ("created_date", "modified_date")
+
+
+######################################################
+# Project order bulk serializers
+######################################################
+
+class UpdateProjectOrderBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ order = serializers.IntegerField()
diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py
index 1a6faeb2..9f9d1049 100644
--- a/taiga/projects/votes/mixins/serializers.py
+++ b/taiga/projects/votes/mixins/serializers.py
@@ -17,11 +17,12 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.fields import MethodField
-class VoteResourceSerializerMixin(serializers.ModelSerializer):
- is_voter = serializers.SerializerMethodField("get_is_voter")
- total_voters = serializers.SerializerMethodField("get_total_voters")
+class VoteResourceSerializerMixin(serializers.LightSerializer):
+ is_voter = MethodField()
+ total_voters = MethodField()
def get_is_voter(self, obj):
# The "is_voted" attribute is attached in the get_queryset of the viewset.
diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py
index 2456e375..50490ba7 100644
--- a/taiga/projects/votes/mixins/viewsets.py
+++ b/taiga/projects/votes/mixins/viewsets.py
@@ -39,14 +39,6 @@ class VotedResourceMixin:
def pre_conditions_on_save(self, obj)
"""
- def attach_votes_attrs_to_queryset(self, queryset):
- qs = attach_total_voters_to_queryset(queryset)
-
- if self.request.user.is_authenticated():
- qs = attach_is_voter_to_queryset(self.request.user, qs)
-
- return qs
-
@detail_route(methods=["POST"])
def upvote(self, request, pk=None):
obj = self.get_object()
diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py
index eb47c9ef..b97bd3bf 100644
--- a/taiga/projects/votes/serializers.py
+++ b/taiga/projects/votes/serializers.py
@@ -17,14 +17,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.contrib.auth import get_user_model
-
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
-class VoterSerializer(serializers.ModelSerializer):
- full_name = serializers.CharField(source='get_full_name', required=False)
+class VoterSerializer(serializers.LightSerializer):
+ id = Field()
+ username = Field()
+ full_name = MethodField()
- class Meta:
- model = get_user_model()
- fields = ('id', 'username', 'full_name')
+ def get_full_name(self, obj):
+ return obj.get_full_name()
diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py
index 291ee284..077abd46 100644
--- a/taiga/projects/votes/utils.py
+++ b/taiga/projects/votes/utils.py
@@ -48,7 +48,7 @@ def attach_total_voters_to_queryset(queryset, as_field="total_voters"):
return qs
-def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"):
+def attach_is_voter_to_queryset(queryset, user, as_field="is_voter"):
"""Attach is_vote boolean to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to
@@ -57,22 +57,26 @@ def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"):
(The other way was to do it in the serializer with some try/except blocks and additional
queries)
- :param user: A users.User object model
:param queryset: A Django queryset object.
+ :param user: A users.User object model
:param as_field: Attach the boolean as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
- sql = ("""SELECT CASE WHEN (SELECT count(*)
- FROM votes_vote
- WHERE votes_vote.content_type_id = {type_id}
- AND votes_vote.object_id = {tbl}.id
- AND votes_vote.user_id = {user_id}) > 0
- THEN TRUE
- ELSE FALSE
- END""")
- sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
+ if user is None or user.is_anonymous():
+ sql = """SELECT false"""
+ else:
+ sql = ("""SELECT CASE WHEN (SELECT count(*)
+ FROM votes_vote
+ WHERE votes_vote.content_type_id = {type_id}
+ AND votes_vote.object_id = {tbl}.id
+ AND votes_vote.user_id = {user_id}) > 0
+ THEN TRUE
+ ELSE FALSE
+ END""")
+ sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
+
qs = queryset.extra(select={as_field: sql})
return qs
diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py
index 2ee75b14..d6a3d44a 100644
--- a/taiga/projects/wiki/api.py
+++ b/taiga/projects/wiki/api.py
@@ -18,26 +18,31 @@
from django.utils.translation import ugettext as _
-from taiga.base.api.permissions import IsAuthenticated
-
-from taiga.base import filters
from taiga.base import exceptions as exc
+from taiga.base import filters
from taiga.base import response
-from taiga.base.api import ModelCrudViewSet, ModelListViewSet
+from taiga.base.api import ModelCrudViewSet
+from taiga.base.api import ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route
-from taiga.projects.models import Project
+
from taiga.mdrender.service import render as mdrender
-from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
+from taiga.projects.history.services import take_snapshot
+from taiga.projects.models import Project
+from taiga.projects.notifications.mixins import WatchedResourceMixin
+from taiga.projects.notifications.mixins import WatchersViewSetMixin
+from taiga.projects.notifications.services import analize_object_for_watchers
+from taiga.projects.notifications.services import send_notifications
from taiga.projects.occ import OCCResourceMixin
-
from . import models
from . import permissions
from . import serializers
+from . import validators
+from . import utils as wiki_utils
class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
@@ -45,6 +50,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
model = models.WikiPage
serializer_class = serializers.WikiPageSerializer
+ validator_class = validators.WikiPageValidator
permission_classes = (permissions.WikiPagePermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ("project", "slug")
@@ -52,7 +58,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
def get_queryset(self):
qs = super().get_queryset()
- qs = self.attach_watchers_attrs_to_queryset(qs)
+ qs = wiki_utils.attach_extra_info(qs, user=self.request.user)
return qs
@list_route(methods=["GET"])
@@ -96,6 +102,32 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer
+ validator_class = validators.WikiLinkValidator
permission_classes = (permissions.WikiLinkPermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ["project"]
+
+ def post_save(self, obj, created=False):
+ if created:
+ self._create_wiki_page_when_create_wiki_link_if_not_exist(self.request, obj)
+ super().pre_save(obj)
+
+ def _create_wiki_page_when_create_wiki_link_if_not_exist(self, request, wiki_link):
+ try:
+ self.check_permissions(request, "create_wiki_page", wiki_link)
+ except exc.PermissionDenied:
+ # Create only the wiki link because the user doesn't have permission.
+ pass
+ else:
+ # Create the wiki link and the wiki page if not exist.
+ wiki_page, created = models.WikiPage.objects.get_or_create(
+ slug=wiki_link.href,
+ project=wiki_link.project,
+ defaults={"owner": self.request.user, "last_modifier": self.request.user})
+
+ if created:
+ # Creaste the new history entre, sSet watcher for the new wiki page
+ # and send notifications about the new page created
+ history = take_snapshot(wiki_page, user=self.request.user)
+ analize_object_for_watchers(wiki_page, history.comment, history.owner)
+ send_notifications(wiki_page, history=history)
diff --git a/taiga/projects/wiki/migrations/0003_auto_20160615_0721.py b/taiga/projects/wiki/migrations/0003_auto_20160615_0721.py
new file mode 100644
index 00000000..1e1876e0
--- /dev/null
+++ b/taiga/projects/wiki/migrations/0003_auto_20160615_0721.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-15 07:21
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('wiki', '0002_remove_wikipage_watchers'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='wikilink',
+ options={'ordering': ['project', 'order', 'id'], 'verbose_name': 'wiki link', 'verbose_name_plural': 'wiki links'},
+ ),
+ migrations.AlterField(
+ model_name='wikilink',
+ name='order',
+ field=models.PositiveSmallIntegerField(default='10000', verbose_name='order'),
+ ),
+ ]
diff --git a/taiga/projects/wiki/migrations/0004_auto_20160928_0540.py b/taiga/projects/wiki/migrations/0004_auto_20160928_0540.py
new file mode 100644
index 00000000..cc3fbacb
--- /dev/null
+++ b/taiga/projects/wiki/migrations/0004_auto_20160928_0540.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-09-28 05:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import taiga.base.utils.time
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('wiki', '0003_auto_20160615_0721'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='wikilink',
+ name='order',
+ field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'),
+ ),
+ ]
diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py
index 659e51f0..1c51fff0 100644
--- a/taiga/projects/wiki/models.py
+++ b/taiga/projects/wiki/models.py
@@ -21,7 +21,10 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
+from django_pglocks import advisory_lock
+
from taiga.base.utils.slug import slugify_uniquely_for_queryset
+from taiga.base.utils.time import timestamp_ms
from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.occ import OCCModelMixin
@@ -70,13 +73,13 @@ class WikiLink(models.Model):
title = models.CharField(max_length=500, null=False, blank=False)
href = models.SlugField(max_length=500, db_index=True, null=False, blank=False,
verbose_name=_("href"))
- order = models.PositiveSmallIntegerField(default=1, null=False, blank=False,
+ order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms,
verbose_name=_("order"))
class Meta:
verbose_name = "wiki link"
verbose_name_plural = "wiki links"
- ordering = ["project", "order"]
+ ordering = ["project", "order", "id"]
unique_together = ("project", "href")
def __str__(self):
@@ -84,7 +87,9 @@ class WikiLink(models.Model):
def save(self, *args, **kwargs):
if not self.href:
- wl_qs = self.project.wiki_links.all()
- self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href")
-
- super().save(*args, **kwargs)
+ with advisory_lock("wiki-page-creation-{}".format(self.project_id)):
+ wl_qs = self.project.wiki_links.all()
+ self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href")
+ super().save(*args, **kwargs)
+ else:
+ super().save(*args, **kwargs)
diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py
index 328e6c3f..5f2311b2 100644
--- a/taiga/projects/wiki/permissions.py
+++ b/taiga/projects/wiki/permissions.py
@@ -20,6 +20,8 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
IsAuthenticated, IsProjectAdmin, AllowAny,
IsSuperUser)
+from taiga.permissions.permissions import CommentAndOrUpdatePerm
+
class WikiPagePermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()
@@ -27,8 +29,8 @@ class WikiPagePermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_wiki_pages')
by_slug_perms = HasProjectPerm('view_wiki_pages')
create_perms = HasProjectPerm('add_wiki_page')
- update_perms = HasProjectPerm('modify_wiki_page')
- partial_update_perms = HasProjectPerm('modify_wiki_page')
+ update_perms = CommentAndOrUpdatePerm('modify_wiki_page', 'comment_wiki_page')
+ partial_update_perms = CommentAndOrUpdatePerm('modify_wiki_page', 'comment_wiki_page')
destroy_perms = HasProjectPerm('delete_wiki_page')
list_perms = AllowAny()
render_perms = AllowAny()
@@ -52,3 +54,4 @@ class WikiLinkPermission(TaigaResourcePermission):
partial_update_perms = HasProjectPerm('modify_wiki_link')
destroy_perms = HasProjectPerm('delete_wiki_link')
list_perms = AllowAny()
+ create_wiki_page_perms = HasProjectPerm('add_wiki_page')
diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py
index 16de19df..a7e36c60 100644
--- a/taiga/projects/wiki/serializers.py
+++ b/taiga/projects/wiki/serializers.py
@@ -17,21 +17,26 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
from taiga.projects.history import services as history_service
-from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.mdrender.service import render as mdrender
-from . import models
+class WikiPageSerializer(WatchedResourceSerializer, serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ slug = Field()
+ content = Field()
+ owner = Field(attr="owner_id")
+ last_modifier = Field(attr="last_modifier_id")
+ created_date = Field()
+ modified_date = Field()
-class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
- html = serializers.SerializerMethodField("get_html")
- editions = serializers.SerializerMethodField("get_editions")
+ html = MethodField()
+ editions = MethodField()
- class Meta:
- model = models.WikiPage
- read_only_fields = ('modified_date', 'created_date', 'owner')
+ version = Field()
def get_html(self, obj):
return mdrender(obj.project, obj.content)
@@ -40,7 +45,9 @@ class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, seri
return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation
-class WikiLinkSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.WikiLink
- read_only_fields = ('href',)
+class WikiLinkSerializer(serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ title = Field()
+ href = Field()
+ order = Field()
diff --git a/taiga/projects/wiki/utils.py b/taiga/projects/wiki/utils.py
new file mode 100644
index 00000000..ecbf7602
--- /dev/null
+++ b/taiga/projects/wiki/utils.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False):
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/wiki/validators.py b/taiga/projects/wiki/validators.py
new file mode 100644
index 00000000..033fac1b
--- /dev/null
+++ b/taiga/projects/wiki/validators.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import validators
+from taiga.projects.notifications.validators import WatchersValidator
+
+from . import models
+
+
+class WikiPageValidator(WatchersValidator, validators.ModelValidator):
+ class Meta:
+ model = models.WikiPage
+ read_only_fields = ('modified_date', 'created_date', 'owner')
+
+
+class WikiLinkValidator(validators.ModelValidator):
+ class Meta:
+ model = models.WikiLink
+ read_only_fields = ('href',)
diff --git a/taiga/routers.py b/taiga/routers.py
index 66e1b9f7..92f2d58c 100644
--- a/taiga/routers.py
+++ b/taiga/routers.py
@@ -54,6 +54,7 @@ from taiga.projects.api import ProjectFansViewSet
from taiga.projects.api import ProjectWatchersViewSet
from taiga.projects.api import MembershipViewSet
from taiga.projects.api import InvitationViewSet
+from taiga.projects.api import EpicStatusViewSet
from taiga.projects.api import UserStoryStatusViewSet
from taiga.projects.api import PointsViewSet
from taiga.projects.api import TaskStatusViewSet
@@ -69,6 +70,7 @@ router.register(r"projects/(?P\d+)/watchers", ProjectWatchersViewSe
router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates")
router.register(r"memberships", MembershipViewSet, base_name="memberships")
router.register(r"invitations", InvitationViewSet, base_name="invitations")
+router.register(r"epic-statuses", EpicStatusViewSet, base_name="epic-statuses")
router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses")
router.register(r"points", PointsViewSet, base_name="points")
router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses")
@@ -79,13 +81,18 @@ router.register(r"severities",SeverityViewSet , base_name="severities")
# Custom Attributes
+from taiga.projects.custom_attributes.api import EpicCustomAttributeViewSet
from taiga.projects.custom_attributes.api import UserStoryCustomAttributeViewSet
from taiga.projects.custom_attributes.api import TaskCustomAttributeViewSet
from taiga.projects.custom_attributes.api import IssueCustomAttributeViewSet
+
+from taiga.projects.custom_attributes.api import EpicCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import UserStoryCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import TaskCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import IssueCustomAttributesValuesViewSet
+router.register(r"epic-custom-attributes", EpicCustomAttributeViewSet,
+ base_name="epic-custom-attributes")
router.register(r"userstory-custom-attributes", UserStoryCustomAttributeViewSet,
base_name="userstory-custom-attributes")
router.register(r"task-custom-attributes", TaskCustomAttributeViewSet,
@@ -93,6 +100,8 @@ router.register(r"task-custom-attributes", TaskCustomAttributeViewSet,
router.register(r"issue-custom-attributes", IssueCustomAttributeViewSet,
base_name="issue-custom-attributes")
+router.register(r"epics/custom-attributes-values", EpicCustomAttributesValuesViewSet,
+ base_name="epic-custom-attributes-values")
router.register(r"userstories/custom-attributes-values", UserStoryCustomAttributesValuesViewSet,
base_name="userstory-custom-attributes-values")
router.register(r"tasks/custom-attributes-values", TaskCustomAttributesValuesViewSet,
@@ -114,58 +123,101 @@ router.register(r"resolver", ResolverViewSet, base_name="resolver")
# Attachments
+from taiga.projects.attachments.api import EpicAttachmentViewSet
from taiga.projects.attachments.api import UserStoryAttachmentViewSet
from taiga.projects.attachments.api import IssueAttachmentViewSet
from taiga.projects.attachments.api import TaskAttachmentViewSet
from taiga.projects.attachments.api import WikiAttachmentViewSet
+router.register(r"epics/attachments", EpicAttachmentViewSet,
+ base_name="epic-attachments")
router.register(r"userstories/attachments", UserStoryAttachmentViewSet,
base_name="userstory-attachments")
-router.register(r"tasks/attachments", TaskAttachmentViewSet, base_name="task-attachments")
-router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments")
-router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments")
+router.register(r"tasks/attachments", TaskAttachmentViewSet,
+ base_name="task-attachments")
+router.register(r"issues/attachments", IssueAttachmentViewSet,
+ base_name="issue-attachments")
+router.register(r"wiki/attachments", WikiAttachmentViewSet,
+ base_name="wiki-attachments")
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.milestones.api import MilestoneWatchersViewSet
+
+from taiga.projects.epics.api import EpicViewSet
+from taiga.projects.epics.api import EpicRelatedUserStoryViewSet
+from taiga.projects.epics.api import EpicVotersViewSet
+from taiga.projects.epics.api import EpicWatchersViewSet
+
from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.userstories.api import UserStoryVotersViewSet
from taiga.projects.userstories.api import UserStoryWatchersViewSet
+
from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.tasks.api import TaskVotersViewSet
from taiga.projects.tasks.api import TaskWatchersViewSet
+
from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import IssueVotersViewSet
from taiga.projects.issues.api import IssueWatchersViewSet
+
from taiga.projects.wiki.api import WikiViewSet
from taiga.projects.wiki.api import WikiLinkViewSet
from taiga.projects.wiki.api import WikiWatchersViewSet
-router.register(r"milestones", MilestoneViewSet, base_name="milestones")
-router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers")
-router.register(r"userstories", UserStoryViewSet, base_name="userstories")
-router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters")
-router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers")
-router.register(r"tasks", TaskViewSet, base_name="tasks")
-router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, base_name="task-voters")
-router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers")
-router.register(r"issues", IssueViewSet, base_name="issues")
-router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet, base_name="issue-voters")
-router.register(r"issues/(?P\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers")
-router.register(r"wiki", WikiViewSet, base_name="wiki")
-router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers")
-router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
+router.register(r"milestones", MilestoneViewSet,
+ base_name="milestones")
+router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet,
+ base_name="milestone-watchers")
+router.register(r"epics", EpicViewSet, base_name="epics")\
+ .register(r"related_userstories", EpicRelatedUserStoryViewSet,
+ base_name="epics-related-userstories",
+ parents_query_lookups=["epic"])
+router.register(r"epics/(?P\d+)/voters", EpicVotersViewSet,
+ base_name="epic-voters")
+router.register(r"epics/(?P\d+)/watchers", EpicWatchersViewSet,
+ base_name="epic-watchers")
+
+router.register(r"userstories", UserStoryViewSet,
+ base_name="userstories")
+router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet,
+ base_name="userstory-voters")
+router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet,
+ base_name="userstory-watchers")
+
+router.register(r"tasks", TaskViewSet,
+ base_name="tasks")
+router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet,
+ base_name="task-voters")
+router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet,
+ base_name="task-watchers")
+
+router.register(r"issues", IssueViewSet,
+ base_name="issues")
+router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet,
+ base_name="issue-voters")
+router.register(r"issues/(?P\d+)/watchers", IssueWatchersViewSet,
+ base_name="issue-watchers")
+
+router.register(r"wiki", WikiViewSet,
+ base_name="wiki")
+router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet,
+ base_name="wiki-watchers")
+router.register(r"wiki-links", WikiLinkViewSet,
+ base_name="wiki-links")
# History & Components
+from taiga.projects.history.api import EpicHistory
from taiga.projects.history.api import UserStoryHistory
from taiga.projects.history.api import TaskHistory
from taiga.projects.history.api import IssueHistory
from taiga.projects.history.api import WikiHistory
+router.register(r"history/epic", EpicHistory, base_name="epic-history")
router.register(r"history/userstory", UserStoryHistory, base_name="userstory-history")
router.register(r"history/task", TaskHistory, base_name="task-history")
router.register(r"history/issue", IssueHistory, base_name="issue-history")
@@ -208,6 +260,12 @@ from taiga.hooks.bitbucket.api import BitBucketViewSet
router.register(r"bitbucket-hook", BitBucketViewSet, base_name="bitbucket-hook")
+# Gogs webhooks
+from taiga.hooks.gogs.api import GogsViewSet
+
+router.register(r"gogs-hook", GogsViewSet, base_name="gogs-hook")
+
+
# Importer
from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet
@@ -217,11 +275,11 @@ router.register(r"exporter", ProjectExporterViewSet, base_name="exporter")
# External apps
from taiga.external_apps.api import Application, ApplicationToken
+
router.register(r"applications", Application, base_name="applications")
router.register(r"application-tokens", ApplicationToken, base_name="application-tokens")
-
# Stats
# - see taiga.stats.routers and taiga.stats.apps
diff --git a/taiga/searches/api.py b/taiga/searches/api.py
index 469c361b..0c252b5a 100644
--- a/taiga/searches/api.py
+++ b/taiga/searches/api.py
@@ -22,7 +22,7 @@ from taiga.base.api import viewsets
from taiga.base import response
from taiga.base.api.utils import get_object_or_404
-from taiga.permissions.service import user_has_perm
+from taiga.permissions.services import user_has_perm
from . import services
from . import serializers
@@ -40,6 +40,10 @@ class SearchViewSet(viewsets.ViewSet):
result = {}
with futures.ThreadPoolExecutor(max_workers=4) as executor:
futures_list = []
+ if user_has_perm(request.user, "view_epics", project):
+ epics_future = executor.submit(self._search_epics, project, text)
+ epics_future.result_key = "epics"
+ futures_list.append(epics_future)
if user_has_perm(request.user, "view_us", project):
uss_future = executor.submit(self._search_user_stories, project, text)
uss_future.result_key = "userstories"
@@ -73,6 +77,11 @@ class SearchViewSet(viewsets.ViewSet):
project_model = apps.get_model("projects", "Project")
return get_object_or_404(project_model, pk=project_id)
+ def _search_epics(self, project, text):
+ queryset = services.search_epics(project, text)
+ serializer = serializers.EpicSearchResultsSerializer(queryset, many=True)
+ return serializer.data
+
def _search_user_stories(self, project, text):
queryset = services.search_user_stories(project, text)
serializer = serializers.UserStorySearchResultsSerializer(queryset, many=True)
diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py
index edc2d1ca..7adc34b2 100644
--- a/taiga/searches/serializers.py
+++ b/taiga/searches/serializers.py
@@ -16,37 +16,56 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.projects.issues.serializers import IssueSerializer
-from taiga.projects.userstories.serializers import UserStorySerializer
-from taiga.projects.tasks.serializers import TaskSerializer
-from taiga.projects.wiki.serializers import WikiPageSerializer
-
-from taiga.projects.issues.models import Issue
-from taiga.projects.userstories.models import UserStory
-from taiga.projects.tasks.models import Task
-from taiga.projects.wiki.models import WikiPage
+from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
-class IssueSearchResultsSerializer(IssueSerializer):
- class Meta:
- model = Issue
- fields = ('id', 'ref', 'subject', 'status', 'assigned_to')
+class EpicSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ assigned_to = Field(attr="assigned_to_id")
-class TaskSearchResultsSerializer(TaskSerializer):
- class Meta:
- model = Task
- fields = ('id', 'ref', 'subject', 'status', 'assigned_to')
+class UserStorySearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ total_points = MethodField()
+ milestone_name = MethodField()
+ milestone_slug = MethodField()
+
+ def get_milestone_name(self, obj):
+ return obj.milestone.name if obj.milestone else None
+
+ def get_milestone_slug(self, obj):
+ return obj.milestone.slug if obj.milestone else None
+
+ def get_total_points(self, obj):
+ assert hasattr(obj, "total_points_attr"), \
+ "instance must have a total_points_attr attribute"
+
+ return obj.total_points_attr
-class UserStorySearchResultsSerializer(UserStorySerializer):
- class Meta:
- model = UserStory
- fields = ('id', 'ref', 'subject', 'status', 'total_points',
- 'milestone_name', 'milestone_slug')
+class TaskSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ assigned_to = Field(attr="assigned_to_id")
-class WikiPageSearchResultsSerializer(WikiPageSerializer):
- class Meta:
- model = WikiPage
- fields = ('id', 'slug')
+class IssueSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ assigned_to = Field(attr="assigned_to_id")
+
+
+class WikiPageSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ slug = Field()
diff --git a/taiga/searches/services.py b/taiga/searches/services.py
index f393844f..adda60bb 100644
--- a/taiga/searches/services.py
+++ b/taiga/searches/services.py
@@ -19,58 +19,78 @@
from django.apps import apps
from django.conf import settings
from taiga.base.utils.db import to_tsquery
+from taiga.projects.userstories.utils import attach_total_points
MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150)
+def search_epics(project, text):
+ model = apps.get_model("epics", "Epic")
+ queryset = model.objects.filter(project_id=project.pk)
+ table = "epics_epic"
+ return _search_items(queryset, table, text)
+
+
def search_user_stories(project, text):
- model_cls = apps.get_model("userstories", "UserStory")
- where_clause = ("to_tsvector('english_nostop', coalesce(userstories_userstory.subject) || ' ' || "
- "coalesce(userstories_userstory.ref) || ' ' || "
- "coalesce(userstories_userstory.description, '')) "
- "@@ to_tsquery('english_nostop', %s)")
-
- if text:
- return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
- .filter(project_id=project.pk)[:MAX_RESULTS])
-
- return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
+ model = apps.get_model("userstories", "UserStory")
+ queryset = model.objects.filter(project_id=project.pk)
+ table = "userstories_userstory"
+ return _search_items(queryset, table, text)
def search_tasks(project, text):
- model_cls = apps.get_model("tasks", "Task")
- where_clause = ("to_tsvector('english_nostop', coalesce(tasks_task.subject, '') || ' ' || "
- "coalesce(tasks_task.ref) || ' ' || "
- "coalesce(tasks_task.description, '')) @@ to_tsquery('english_nostop', %s)")
-
- if text:
- return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
- .filter(project_id=project.pk)[:MAX_RESULTS])
-
- return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
+ model = apps.get_model("tasks", "Task")
+ queryset = model.objects.filter(project_id=project.pk)
+ table = "tasks_task"
+ return _search_items(queryset, table, text)
def search_issues(project, text):
- model_cls = apps.get_model("issues", "Issue")
- where_clause = ("to_tsvector('english_nostop', coalesce(issues_issue.subject) || ' ' || "
- "coalesce(issues_issue.ref) || ' ' || "
- "coalesce(issues_issue.description, '')) @@ to_tsquery('english_nostop', %s)")
-
- if text:
- return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
- .filter(project_id=project.pk)[:MAX_RESULTS])
-
- return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
+ model = apps.get_model("issues", "Issue")
+ queryset = model.objects.filter(project_id=project.pk)
+ table = "issues_issue"
+ return _search_items(queryset, table, text)
def search_wiki_pages(project, text):
- model_cls = apps.get_model("wiki", "WikiPage")
- where_clause = ("to_tsvector('english_nostop', coalesce(wiki_wikipage.slug) || ' ' || "
- "coalesce(wiki_wikipage.content, '')) "
- "@@ to_tsquery('english_nostop', %s)")
+ model = apps.get_model("wiki", "WikiPage")
+ queryset = model.objects.filter(project_id=project.pk)
+ tsquery = "to_tsquery('english_nostop', %s)"
+ tsvector = """
+ setweight(to_tsvector('english_nostop', coalesce(wiki_wikipage.slug)), 'A') ||
+ setweight(to_tsvector('english_nostop', coalesce(wiki_wikipage.content)), 'B')
+ """
+
+ return _search_by_query(queryset, tsquery, tsvector, text)
+
+
+def _search_items(queryset, table, text):
+ tsquery = "to_tsquery('english_nostop', %s)"
+ tsvector = """
+ setweight(to_tsvector('english_nostop',
+ coalesce({table}.subject) || ' ' ||
+ coalesce({table}.ref)), 'A') ||
+ setweight(to_tsvector('english_nostop', coalesce(inmutable_array_to_string({table}.tags))), 'B') ||
+ setweight(to_tsvector('english_nostop', coalesce({table}.description)), 'C')
+ """.format(table=table)
+ return _search_by_query(queryset, tsquery, tsvector, text)
+
+
+def _search_by_query(queryset, tsquery, tsvector, text):
+ select = {
+ "rank": "ts_rank({tsvector},{tsquery})".format(tsquery=tsquery,
+ tsvector=tsvector),
+ }
+ order_by = ["-rank", ]
+ where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery,
+ tsvector=tsvector), ]
if text:
- return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
- .filter(project_id=project.pk)[:MAX_RESULTS])
+ queryset = queryset.extra(select=select,
+ select_params=[to_tsquery(text)],
+ where=where,
+ params=[to_tsquery(text)],
+ order_by=order_by)
- return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
+ queryset = attach_total_points(queryset)
+ return queryset[:MAX_RESULTS]
diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py
index b0bf8e13..2ebe5fa2 100644
--- a/taiga/timeline/api.py
+++ b/taiga/timeline/api.py
@@ -18,10 +18,8 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
-from django.apps import apps
from taiga.base import response
-from taiga.base.api.utils import get_object_or_404
from taiga.base.api import ReadOnlyListViewSet
from . import serializers
@@ -36,7 +34,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
def get_content_type(self):
app_name, model = self.content_type.split(".", 1)
- return get_object_or_404(ContentType, app_label=app_name, model=model)
+ return ContentType.objects.get_by_natural_key(app_name, model)
def get_queryset(self):
ct = self.get_content_type()
@@ -87,6 +85,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
event_type::text = ANY('{issues.issue.change,
tasks.task.change,
userstories.userstory.change,
+ epics.epic.change,
wiki.wikipage.change}'::text[])
)
"""])
@@ -94,6 +93,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
qs = qs.exclude(event_type__in=["issues.issue.delete",
"tasks.task.delete",
"userstories.userstory.delete",
+ "epics.epic.delete",
"wiki.wikipage.delete",
"projects.project.change"])
diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py
index 7b193552..fa951716 100644
--- a/taiga/timeline/apps.py
+++ b/taiga/timeline/apps.py
@@ -33,8 +33,8 @@ class TimelineAppConfig(AppConfig):
sender=apps.get_model("history", "HistoryEntry"),
dispatch_uid="timeline")
signals.post_save.connect(handlers.create_membership_push_to_timeline,
- sender=apps.get_model("projects", "Membership"))
+ sender=apps.get_model("projects", "Membership"))
signals.pre_delete.connect(handlers.delete_membership_push_to_timeline,
- sender=apps.get_model("projects", "Membership"))
+ sender=apps.get_model("projects", "Membership"))
signals.post_save.connect(handlers.create_user_push_to_timeline,
- sender=get_user_model())
+ sender=get_user_model())
diff --git a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py
index 07290281..f3a5fa57 100644
--- a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py
+++ b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py
@@ -25,8 +25,9 @@ from django.core.management.base import BaseCommand
from django.db.models import Model
from django.test.utils import override_settings
-from taiga.timeline.service import (_get_impl_key_from_model,
- _timeline_impl_map, extract_user_info)
+from taiga.timeline.service import _get_impl_key_from_model,
+from taiga.timeline.service import _timeline_impl_map,
+from taiga.timeline.service import extract_user_info)
from taiga.timeline.models import Timeline
from taiga.timeline.signals import _push_to_timelines
@@ -54,7 +55,8 @@ class BulkCreator(object):
bulk_creator = BulkCreator()
-def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object,
+ namespace:str="default", extra_data:dict={}):
assert isinstance(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model"
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
diff --git a/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py
index 090cf5f6..6c37b17c 100644
--- a/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py
+++ b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py
@@ -40,7 +40,9 @@ def update_timeline(initial_date, final_date):
print("Generating tasks indexed by id dict")
task_ids = timelines.values_list("object_id", flat=True)
- tasks_per_id = {task.id: task for task in Task.objects.filter(id__in=task_ids).select_related("user_story").iterator()}
+
+ tasks_iterator = Task.objects.filter(id__in=task_ids).select_related("user_story").iterator()
+ tasks_per_id = {task.id: task for task in tasks_iterator}
del task_ids
counter = 1
diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py
index 947a7418..674f6b9d 100644
--- a/taiga/timeline/management/commands/rebuild_timeline.py
+++ b/taiga/timeline/management/commands/rebuild_timeline.py
@@ -58,7 +58,8 @@ class BulkCreator(object):
bulk_creator = BulkCreator()
-def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object,
+ namespace:str="default", extra_data:dict={}):
assert isinstance(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model"
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
@@ -102,11 +103,13 @@ def generate_timeline(initial_date, final_date, project_id):
if project_id:
project = Project.objects.get(id=project_id)
- us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id", flat=True)]
+ epic_keys = ['epics.epic:%s'%(id) for id in project.epics.values_list("id", flat=True)]
+ us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id",
+ flat=True)]
tasks_keys = ['tasks.task:%s'%(id) for id in project.tasks.values_list("id", flat=True)]
issue_keys = ['issues.issue:%s'%(id) for id in project.issues.values_list("id", flat=True)]
wiki_keys = ['wiki.wikipage:%s'%(id) for id in project.wiki_pages.values_list("id", flat=True)]
- keys = us_keys + tasks_keys + issue_keys + wiki_keys
+ keys = epic_keys + us_keys + tasks_keys + issue_keys + wiki_keys
projects = projects.filter(id=project_id)
history_entries = history_entries.filter(key__in=keys)
@@ -116,12 +119,13 @@ def generate_timeline(initial_date, final_date, project_id):
_push_to_timelines(project, membership.user, membership, "create", membership.created_at)
for project in projects.iterator():
- print("Project:", bulk_creator.created)
+ print("Project:", project)
extra_data = {
"values_diff": {},
"user": extract_user_info(project.owner),
}
- _push_to_timelines(project, project.owner, project, "create", project.created_date, extra_data=extra_data)
+ _push_to_timelines(project, project.owner, project, "create", project.created_date,
+ extra_data=extra_data)
del extra_data
for historyEntry in history_entries.iterator():
diff --git a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py
index 2f2ae1b5..f4c1a0a4 100644
--- a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py
+++ b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py
@@ -31,7 +31,7 @@ class Command(BaseCommand):
total = Project.objects.count()
for count,project in enumerate(Project.objects.order_by("id")):
- print("""***********************************
- %s/%s %s
-***********************************"""%(count+1, total, project.name))
+ print("***********************************\n",
+ " {}/{} {}\n".format(count+1, total, project.name),
+ "***********************************")
call_command("rebuild_timeline", project=project.id)
diff --git a/taiga/timeline/migrations/0005_auto_20160706_0723.py b/taiga/timeline/migrations/0005_auto_20160706_0723.py
new file mode 100644
index 00000000..7ac9fa9c
--- /dev/null
+++ b/taiga/timeline/migrations/0005_auto_20160706_0723.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-06 07:23
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('timeline', '0004_auto_20150603_1312'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='timeline',
+ name='created',
+ field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
+ ),
+ ]
diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py
index c71188f7..ebee7da5 100644
--- a/taiga/timeline/models.py
+++ b/taiga/timeline/models.py
@@ -20,13 +20,12 @@ from django.db import models
from django_pgjson.fields import JsonField
from django.utils import timezone
-from django.core.exceptions import ValidationError
-
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from taiga.projects.models import Project
+
class Timeline(models.Model):
content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
object_id = models.PositiveIntegerField()
@@ -36,12 +35,11 @@ class Timeline(models.Model):
project = models.ForeignKey(Project, null=True)
data = JsonField()
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
- created = models.DateTimeField(default=timezone.now)
+ created = models.DateTimeField(default=timezone.now, db_index=True)
class Meta:
index_together = [('content_type', 'object_id', 'namespace'), ]
-
# Register all implementations
from .timeline_implementations import *
diff --git a/taiga/timeline/permissions.py b/taiga/timeline/permissions.py
index 2ee25e58..61a20130 100644
--- a/taiga/timeline/permissions.py
+++ b/taiga/timeline/permissions.py
@@ -16,13 +16,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm,
- AllowAny)
+from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsSuperUser
+from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin
class UserTimelinePermission(TaigaResourcePermission):
+ enought_perms = IsSuperUser()
+ global_perms = None
retrieve_perms = AllowAny()
class ProjectTimelinePermission(TaigaResourcePermission):
+ enought_perms = IsProjectAdmin() | IsSuperUser()
+ global_perms = None
retrieve_perms = HasProjectPerm('view_project')
diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py
index a6be6944..0b831d04 100644
--- a/taiga/timeline/serializers.py
+++ b/taiga/timeline/serializers.py
@@ -16,26 +16,33 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.apps import apps
from django.contrib.auth import get_user_model
-from django.forms import widgets
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
-from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
+from taiga.base.fields import Field, MethodField
+from taiga.users.services import get_user_photo_url, get_user_big_photo_url
+from taiga.users.gravatar import get_user_gravatar_id
from . import models
-from . import service
-class TimelineSerializer(serializers.ModelSerializer):
+class TimelineSerializer(serializers.LightSerializer):
data = serializers.SerializerMethodField("get_data")
+ id = Field()
+ content_type = Field(attr="content_type_id")
+ object_id = Field()
+ namespace = Field()
+ event_type = Field()
+ project = Field(attr="project_id")
+ data = MethodField()
+ data_content_type = Field(attr="data_content_type_id")
+ created = Field()
class Meta:
model = models.Timeline
def get_data(self, obj):
- #Updates the data user info saved if the user exists
+ # Updates the data user info saved if the user exists
if hasattr(obj, "_prefetched_user"):
user = obj._prefetched_user
else:
@@ -50,8 +57,9 @@ class TimelineSerializer(serializers.ModelSerializer):
obj.data["user"] = {
"id": user.pk,
"name": user.get_full_name(),
- "photo": get_photo_or_gravatar_url(user),
- "big_photo": get_big_photo_or_gravatar_url(user),
+ "photo": get_user_photo_url(user),
+ "big_photo": get_user_big_photo_url(user),
+ "gravatar_id": get_user_gravatar_id(user),
"username": user.username,
"is_profile_visible": user.is_active and not user.is_system,
"date_joined": user.date_joined
diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py
index f99f795e..03ca3e96 100644
--- a/taiga/timeline/service.py
+++ b/taiga/timeline/service.py
@@ -27,33 +27,33 @@ from functools import partial, wraps
from taiga.base.utils.db import get_typename_for_model_class
from taiga.celery import app
-from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
_timeline_impl_map = {}
-def _get_impl_key_from_model(model:Model, event_type:str):
+def _get_impl_key_from_model(model: Model, event_type: str):
if issubclass(model, Model):
typename = get_typename_for_model_class(model)
return _get_impl_key_from_typename(typename, event_type)
raise Exception("Not valid model parameter")
-def _get_impl_key_from_typename(typename:str, event_type:str):
+def _get_impl_key_from_typename(typename: str, event_type: str):
if isinstance(typename, str):
return "{0}.{1}".format(typename, event_type)
raise Exception("Not valid typename parameter")
-def build_user_namespace(user:object):
+def build_user_namespace(user: object):
return "{0}:{1}".format("user", user.id)
-def build_project_namespace(project:object):
+def build_project_namespace(project: object):
return "{0}:{1}".format("project", project.id)
-def _add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object,
+ namespace: str="default", extra_data: dict={}):
assert isinstance(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model"
from .models import Timeline
@@ -75,12 +75,14 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, created
)
-def _add_to_objects_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object,
+ namespace: str="default", extra_data: dict={}):
for obj in objects:
_add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data)
-def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object,
+ namespace: str="default", extra_data: dict={}):
if isinstance(objects, Model):
_add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data)
elif isinstance(objects, QuerySet) or isinstance(objects, list):
@@ -90,7 +92,9 @@ def _push_to_timeline(objects, instance:object, event_type:str, created_datetime
@app.task
-def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type, created_datetime, extra_data={}):
+def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type,
+ created_datetime, extra_data={}):
+
ObjModel = apps.get_model(obj_app_label, obj_model_name)
try:
obj = ObjModel.objects.get(id=obj_id)
@@ -111,10 +115,10 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
except projectModel.DoesNotExist:
return
- ## Project timeline
+ # Project timeline
_push_to_timeline(project, obj, event_type, created_datetime,
- namespace=build_project_namespace(project),
- extra_data=extra_data)
+ namespace=build_project_namespace(project),
+ extra_data=extra_data)
project.refresh_totals()
@@ -122,14 +126,14 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
related_people = obj.get_related_people()
_push_to_timeline(related_people, obj, event_type, created_datetime,
- namespace=build_user_namespace(user),
- extra_data=extra_data)
+ namespace=build_user_namespace(user),
+ extra_data=extra_data)
else:
# Actions not related with a project
- ## - Me
+ # - Me
_push_to_timeline(user, obj, event_type, created_datetime,
- namespace=build_user_namespace(user),
- extra_data=extra_data)
+ namespace=build_user_namespace(user),
+ extra_data=extra_data)
def get_timeline(obj, namespace=None):
@@ -141,7 +145,6 @@ def get_timeline(obj, namespace=None):
if namespace is not None:
timeline = timeline.filter(namespace=namespace)
- timeline = timeline.select_related("project")
timeline = timeline.order_by("-created", "-id")
return timeline
@@ -156,22 +159,23 @@ def filter_timeline_for_user(timeline, user):
# Filtering private project with some public parts
content_types = {
- "view_project": ContentType.objects.get(app_label="projects", model="project"),
- "view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"),
- "view_us": ContentType.objects.get(app_label="userstories", model="userstory"),
- "view_tasks": ContentType.objects.get(app_label="tasks", model="task"),
- "view_issues": ContentType.objects.get(app_label="issues", model="issue"),
- "view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"),
- "view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"),
+ "view_project": ContentType.objects.get_by_natural_key("projects", "project"),
+ "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"),
+ "view_epics": ContentType.objects.get_by_natural_key("epics", "epic"),
+ "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"),
+ "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"),
+ "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"),
+ "view_wiki_pages": ContentType.objects.get_by_natural_key("wiki", "wikipage"),
+ "view_wiki_links": ContentType.objects.get_by_natural_key("wiki", "wikilink"),
}
for content_type_key, content_type in content_types.items():
tl_filter |= Q(project__is_private=True,
- project__anon_permissions__contains=[content_type_key],
- data_content_type=content_type)
+ project__anon_permissions__contains=[content_type_key],
+ data_content_type=content_type)
# There is no specific permission for seeing new memberships
- membership_content_type = ContentType.objects.get(app_label="projects", model="membership")
+ membership_content_type = ContentType.objects.get_by_natural_key(app_label="projects", model="membership")
tl_filter |= Q(project__is_private=True,
project__anon_permissions__contains=["view_project"],
data_content_type=membership_content_type)
@@ -183,7 +187,8 @@ def filter_timeline_for_user(timeline, user):
if membership.is_admin:
tl_filter |= Q(project=membership.project)
else:
- data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions]))
+ data_content_types = list(filter(None, [content_types.get(a, None) for a in
+ membership.role.permissions]))
data_content_types.append(membership_content_type)
tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types)
@@ -214,7 +219,7 @@ def get_project_timeline(project, accessing_user=None):
return timeline
-def register_timeline_implementation(typename:str, event_type:str, fn=None):
+def register_timeline_implementation(typename: str, event_type: str, fn=None):
assert isinstance(typename, str), "typename must be a string"
assert isinstance(event_type, str), "event_type must be a string"
@@ -231,7 +236,6 @@ def register_timeline_implementation(typename:str, event_type:str, fn=None):
return _wrapper
-
def extract_project_info(instance):
return {
"id": instance.pk,
@@ -255,7 +259,7 @@ def extract_milestone_info(instance):
}
-def extract_userstory_info(instance):
+def extract_epic_info(instance):
return {
"id": instance.pk,
"ref": instance.ref,
@@ -263,6 +267,26 @@ def extract_userstory_info(instance):
}
+def extract_userstory_info(instance, include_project=False):
+ userstory_info = {
+ "id": instance.pk,
+ "ref": instance.ref,
+ "subject": instance.subject,
+ }
+
+ if include_project:
+ userstory_info["project"] = extract_project_info(instance.project)
+
+ return userstory_info
+
+
+def extract_related_userstory_info(instance):
+ return {
+ "id": instance.pk,
+ "subject": instance.user_story.subject
+ }
+
+
def extract_issue_info(instance):
return {
"id": instance.pk,
diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py
index 0b8f851d..7f754b63 100644
--- a/taiga/timeline/signals.py
+++ b/taiga/timeline/signals.py
@@ -36,9 +36,23 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
ct = ContentType.objects.get_for_model(obj)
if settings.CELERY_ENABLED:
- connection.on_commit(lambda: push_to_timelines.delay(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data))
+ connection.on_commit(lambda: push_to_timelines.delay(project_id,
+ user.id,
+ ct.app_label,
+ ct.model,
+ obj.id,
+ event_type,
+ created_datetime,
+ extra_data=extra_data))
else:
- push_to_timelines(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data)
+ push_to_timelines(project_id,
+ user.id,
+ ct.app_label,
+ ct.model,
+ obj.id,
+ event_type,
+ created_datetime,
+ extra_data=extra_data)
def _clean_description_fields(values_diff):
@@ -50,7 +64,6 @@ def _clean_description_fields(values_diff):
def on_new_history_entry(sender, instance, created, **kwargs):
-
if instance._importing:
return
@@ -87,6 +100,10 @@ def on_new_history_entry(sender, instance, created, **kwargs):
if instance.delete_comment_date:
extra_data["comment_deleted"] = True
+ # Detect edited comment
+ if instance.comment_versions is not None and len(instance.comment_versions)>0:
+ extra_data["comment_edited"] = True
+
created_datetime = instance.created_at
_push_to_timelines(project, user, obj, event_type, created_datetime, extra_data=extra_data)
diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py
index cff785ad..1d480e82 100644
--- a/taiga/timeline/timeline_implementations.py
+++ b/taiga/timeline/timeline_implementations.py
@@ -19,11 +19,12 @@
from taiga.timeline.service import register_timeline_implementation
from . import service
+
@register_timeline_implementation("projects.project", "create")
@register_timeline_implementation("projects.project", "change")
@register_timeline_implementation("projects.project", "delete")
def project_timeline(instance, extra_data={}):
- result ={
+ result = {
"project": service.extract_project_info(instance),
}
result.update(extra_data)
@@ -33,8 +34,8 @@ def project_timeline(instance, extra_data={}):
@register_timeline_implementation("milestones.milestone", "create")
@register_timeline_implementation("milestones.milestone", "change")
@register_timeline_implementation("milestones.milestone", "delete")
-def project_timeline(instance, extra_data={}):
- result ={
+def milestone_timeline(instance, extra_data={}):
+ result = {
"milestone": service.extract_milestone_info(instance),
"project": service.extract_project_info(instance.project),
}
@@ -42,11 +43,37 @@ def project_timeline(instance, extra_data={}):
return result
+@register_timeline_implementation("epics.epic", "create")
+@register_timeline_implementation("epics.epic", "change")
+@register_timeline_implementation("epics.epic", "delete")
+def epic_timeline(instance, extra_data={}):
+ result = {
+ "epic": service.extract_epic_info(instance),
+ "project": service.extract_project_info(instance.project),
+ }
+ result.update(extra_data)
+ return result
+
+
+@register_timeline_implementation("epics.relateduserstory", "create")
+@register_timeline_implementation("epics.relateduserstory", "change")
+@register_timeline_implementation("epics.relateduserstory", "delete")
+def epic_related_userstory_timeline(instance, extra_data={}):
+ result = {
+ "relateduserstory": service.extract_related_userstory_info(instance),
+ "epic": service.extract_epic_info(instance.epic),
+ "userstory": service.extract_userstory_info(instance.user_story, include_project=True),
+ "project": service.extract_project_info(instance.project),
+ }
+ result.update(extra_data)
+ return result
+
+
@register_timeline_implementation("userstories.userstory", "create")
@register_timeline_implementation("userstories.userstory", "change")
@register_timeline_implementation("userstories.userstory", "delete")
def userstory_timeline(instance, extra_data={}):
- result ={
+ result = {
"userstory": service.extract_userstory_info(instance),
"project": service.extract_project_info(instance.project),
}
@@ -62,7 +89,7 @@ def userstory_timeline(instance, extra_data={}):
@register_timeline_implementation("issues.issue", "change")
@register_timeline_implementation("issues.issue", "delete")
def issue_timeline(instance, extra_data={}):
- result ={
+ result = {
"issue": service.extract_issue_info(instance),
"project": service.extract_project_info(instance.project),
}
@@ -74,7 +101,7 @@ def issue_timeline(instance, extra_data={}):
@register_timeline_implementation("tasks.task", "change")
@register_timeline_implementation("tasks.task", "delete")
def task_timeline(instance, extra_data={}):
- result ={
+ result = {
"task": service.extract_task_info(instance),
"project": service.extract_project_info(instance.project),
}
@@ -85,11 +112,12 @@ def task_timeline(instance, extra_data={}):
result.update(extra_data)
return result
+
@register_timeline_implementation("wiki.wikipage", "create")
@register_timeline_implementation("wiki.wikipage", "change")
@register_timeline_implementation("wiki.wikipage", "delete")
def wiki_page_timeline(instance, extra_data={}):
- result ={
+ result = {
"wikipage": service.extract_wiki_page_info(instance),
"project": service.extract_project_info(instance.project),
}
@@ -100,7 +128,7 @@ def wiki_page_timeline(instance, extra_data={}):
@register_timeline_implementation("projects.membership", "create")
@register_timeline_implementation("projects.membership", "delete")
def membership_timeline(instance, extra_data={}):
- result = {
+ result = {
"user": service.extract_user_info(instance.user),
"project": service.extract_project_info(instance.project),
"role": service.extract_role_info(instance.role),
@@ -108,9 +136,10 @@ def membership_timeline(instance, extra_data={}):
result.update(extra_data)
return result
+
@register_timeline_implementation("users.user", "create")
def user_timeline(instance, extra_data={}):
- result = {
+ result = {
"user": service.extract_user_info(instance),
}
result.update(extra_data)
diff --git a/taiga/users/api.py b/taiga/users/api.py
index a02e1576..a9f1c957 100644
--- a/taiga/users/api.py
+++ b/taiga/users/api.py
@@ -19,7 +19,6 @@
import uuid
from django.apps import apps
-from django.db.models import Q, F
from django.utils.translation import ugettext as _
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
@@ -28,21 +27,22 @@ from django.conf import settings
from taiga.base import exceptions as exc
from taiga.base import filters
from taiga.base import response
+from taiga.base.utils.dicts import into_namedtuple
from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
-from taiga.base.filters import PermissionBasedFilterBackend
+from taiga.base.api.fields import validate_user_email_allowed_domains
from taiga.base.api.utils import get_object_or_404
from taiga.base.filters import MembersFilterBackend
from taiga.base.mails import mail_builder
-from taiga.projects.votes import services as votes_service
from taiga.users.services import get_user_by_username_or_email
from easy_thumbnails.source_generators import pil_image
from . import models
from . import serializers
+from . import validators
from . import permissions
from . import filters as user_filters
from . import services
@@ -53,6 +53,8 @@ class UsersViewSet(ModelCrudViewSet):
permission_classes = (permissions.UserPermission,)
admin_serializer_class = serializers.UserAdminSerializer
serializer_class = serializers.UserSerializer
+ admin_validator_class = validators.UserAdminValidator
+ validator_class = validators.UserValidator
queryset = models.User.objects.all().prefetch_related("memberships")
filter_backends = (MembersFilterBackend,)
@@ -64,6 +66,14 @@ class UsersViewSet(ModelCrudViewSet):
return self.serializer_class
+ def get_validator_class(self):
+ if self.action in ["partial_update", "update", "retrieve", "by_username"]:
+ user = self.object
+ if self.request.user == user or self.request.user.is_superuser:
+ return self.admin_validator_class
+
+ return self.validator_class
+
def create(self, *args, **kwargs):
raise exc.NotSupported()
@@ -86,7 +96,7 @@ class UsersViewSet(ModelCrudViewSet):
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
- #TODO: commit_on_success
+ # TODO: commit_on_success
def partial_update(self, request, *args, **kwargs):
"""
We must detect if the user is trying to change his email so we can
@@ -96,15 +106,14 @@ class UsersViewSet(ModelCrudViewSet):
user = self.get_object()
self.check_permissions(request, "update", user)
- ret = super().partial_update(request, *args, **kwargs)
-
new_email = request.DATA.get('email', None)
if new_email is not None:
valid_new_email = True
- duplicated_email = models.User.objects.filter(email = new_email).exists()
+ duplicated_email = models.User.objects.filter(email=new_email).exists()
try:
validate_email(new_email)
+ validate_user_email_allowed_domains(new_email)
except ValidationError:
valid_new_email = False
@@ -115,14 +124,21 @@ class UsersViewSet(ModelCrudViewSet):
elif not valid_new_email:
raise exc.WrongArguments(_("Not valid email"))
- #We need to generate a token for the email
+ # We need to generate a token for the email
request.user.email_token = str(uuid.uuid1())
request.user.new_email = new_email
request.user.save(update_fields=["email_token", "new_email"])
- email = mail_builder.change_email(request.user.new_email, {"user": request.user,
- "lang": request.user.lang})
+ email = mail_builder.change_email(
+ request.user.new_email,
+ {
+ "user": request.user,
+ "lang": request.user.lang
+ }
+ )
email.send()
+ ret = super().partial_update(request, *args, **kwargs)
+
return ret
def destroy(self, request, pk=None):
@@ -165,16 +181,16 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "change_password_from_recovery", None)
- serializer = serializers.RecoverySerializer(data=request.DATA, many=False)
- if not serializer.is_valid():
+ validator = validators.RecoveryValidator(data=request.DATA, many=False)
+ if not validator.is_valid():
raise exc.WrongArguments(_("Token is invalid"))
try:
- user = models.User.objects.get(token=serializer.data["token"])
+ user = models.User.objects.get(token=validator.data["token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Token is invalid"))
- user.set_password(serializer.data["password"])
+ user.set_password(validator.data["password"])
user.token = None
user.save(update_fields=["password", "token"])
@@ -247,13 +263,13 @@ class UsersViewSet(ModelCrudViewSet):
"""
Verify the email change to current logged user.
"""
- serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False)
- if not serializer.is_valid():
+ validator = validators.ChangeEmailValidator(data=request.DATA, many=False)
+ if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?"))
try:
- user = models.User.objects.get(email_token=serializer.data["email_token"])
+ user = models.User.objects.get(email_token=validator.data["email_token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?"))
@@ -280,14 +296,14 @@ class UsersViewSet(ModelCrudViewSet):
"""
Cancel an account via token
"""
- serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False)
- if not serializer.is_valid():
+ validator = validators.CancelAccountValidator(data=request.DATA, many=False)
+ if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
try:
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
- user = get_user_for_token(serializer.data["cancel_token"], "cancel_account",
- max_age=max_age_cancel_account)
+ user = get_user_for_token(validator.data["cancel_token"], "cancel_account",
+ max_age=max_age_cancel_account)
except exc.NotAuthenticated:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
@@ -305,7 +321,7 @@ class UsersViewSet(ModelCrudViewSet):
self.object_list = user_filters.ContactsFilterBackend().filter_queryset(
user, request, self.get_queryset(), self).extra(
- select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name")
+ select={"complete_user_name": "concat(full_name, username)"}).order_by("complete_user_name")
page = self.paginate_queryset(self.object_list)
if page is not None:
@@ -349,10 +365,10 @@ class UsersViewSet(ModelCrudViewSet):
for elem in elements:
if elem["type"] == "project":
# projects are liked objects
- response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data )
+ response_data.append(serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args_liked).data)
else:
# stories, tasks and issues are voted objects
- response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data )
+ response_data.append(serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args_voted).data)
return response.Ok(response_data)
@@ -374,7 +390,7 @@ class UsersViewSet(ModelCrudViewSet):
"user_likes": services.get_liked_content_for_user(request.user),
}
- response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements]
+ response_data = [serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data)
@@ -397,17 +413,18 @@ class UsersViewSet(ModelCrudViewSet):
"user_votes": services.get_voted_content_for_user(request.user),
}
- response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements]
+ response_data = [serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data)
-######################################################
-## Role
-######################################################
+######################################################
+# Role
+######################################################
class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Role
serializer_class = serializers.RoleSerializer
+ validator_class = validators.RoleValidator
permission_classes = (permissions.RolesPermission, )
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
diff --git a/taiga/users/filters.py b/taiga/users/filters.py
index 4e4dc116..46dd88ac 100644
--- a/taiga/users/filters.py
+++ b/taiga/users/filters.py
@@ -16,11 +16,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.apps import apps
-
from taiga.base.filters import PermissionBasedFilterBackend
from . import services
+
class ContactsFilterBackend(PermissionBasedFilterBackend):
def filter_queryset(self, user, request, queryset, view):
qs = queryset.filter(is_active=True)
diff --git a/taiga/users/gravatar.py b/taiga/users/gravatar.py
index 7793e59d..b8329d95 100644
--- a/taiga/users/gravatar.py
+++ b/taiga/users/gravatar.py
@@ -18,45 +18,22 @@
# along with this program. If not, see .
import hashlib
-import copy
-
-from urllib.parse import urlencode
-
-from django.conf import settings
-from django.templatetags.static import static
-
-GRAVATAR_BASE_URL = "//www.gravatar.com/avatar/{}?{}"
-def get_gravatar_url(email: str, **options) -> str:
- """Get the gravatar url associated to an email.
+def get_gravatar_id(email: str) -> str:
+ """Get the gravatar id associated to an email.
- :param options: Additional options to gravatar.
- - `default` defines what image url to show if no gravatar exists
- - `size` defines the size of the avatar.
-
- :return: Gravatar url.
+ :return: Gravatar id.
"""
- params = copy.copy(options)
+ return hashlib.md5(email.lower().encode()).hexdigest()
- default_avatar = getattr(settings, "GRAVATAR_DEFAULT_AVATAR", None)
- default_size = getattr(settings, "GRAVATAR_AVATAR_SIZE", None)
+def get_user_gravatar_id(user: object) -> str:
+ """Get the gravatar id associated to a user.
- avatar = options.get("default", None)
- size = options.get("size", None)
+ :return: Gravatar id.
+ """
+ if user and user.email:
+ return get_gravatar_id(user.email)
- if avatar:
- params["default"] = avatar
- elif default_avatar:
- params["default"] = static(default_avatar)
-
- if size:
- params["size"] = size
- elif default_size:
- params["size"] = default_size
-
- email_hash = hashlib.md5(email.lower().encode()).hexdigest()
- url = GRAVATAR_BASE_URL.format(email_hash, urlencode(params))
-
- return url
+ return None
diff --git a/taiga/users/migrations/0019_auto_20160519_1058.py b/taiga/users/migrations/0019_auto_20160519_1058.py
new file mode 100644
index 00000000..69780084
--- /dev/null
+++ b/taiga/users/migrations/0019_auto_20160519_1058.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-19 10:58
+from __future__ import unicode_literals
+
+from django.db import migrations
+import djorm_pgarray.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0018_remove_vote_issues_in_roles_permissions_field'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='permissions',
+ field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', default=[], verbose_name='permissions'),
+ ),
+ ]
diff --git a/taiga/users/migrations/0020_auto_20160525_1229.py b/taiga/users/migrations/0020_auto_20160525_1229.py
new file mode 100644
index 00000000..5e73a57e
--- /dev/null
+++ b/taiga/users/migrations/0020_auto_20160525_1229.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-05-25 12:29
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+UPDATE_ROLES_PERMISSIONS_SQL = """
+ UPDATE users_role
+ SET
+ PERMISSIONS = array_append(PERMISSIONS, '{comment_permission}')
+ WHERE
+ '{base_permission}' = ANY(PERMISSIONS)
+ AND
+ NOT '{comment_permission}' = ANY(PERMISSIONS)
+"""
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0019_auto_20160519_1058'),
+ ]
+
+ operations = [
+ # user stories
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_us",
+ comment_permission="comment_us")
+ ),
+
+ # tasks
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_task",
+ comment_permission="comment_task")
+ ),
+
+ # issues
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_issue",
+ comment_permission="comment_issue")
+ ),
+
+ # wiki pages
+ migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format(
+ base_permission="modify_wiki_page",
+ comment_permission="comment_wiki_page")
+ )
+ ]
diff --git a/taiga/users/migrations/0021_auto_20160614_1201.py b/taiga/users/migrations/0021_auto_20160614_1201.py
new file mode 100644
index 00000000..a9f1bb98
--- /dev/null
+++ b/taiga/users/migrations/0021_auto_20160614_1201.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-14 12:01
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0020_auto_20160525_1229'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='permissions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'),
+ ),
+ ]
diff --git a/taiga/users/migrations/0022_auto_20160629_1443.py b/taiga/users/migrations/0022_auto_20160629_1443.py
new file mode 100644
index 00000000..68a65443
--- /dev/null
+++ b/taiga/users/migrations/0022_auto_20160629_1443.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-06-29 14:43
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0021_auto_20160614_1201'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='role',
+ name='permissions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epics', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'),
+ ),
+ ]
diff --git a/taiga/users/models.py b/taiga/users/models.py
index c6e17351..45908a10 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -26,6 +26,7 @@ from django.apps.config import MODELS_MODULE_NAME
from django.conf import settings
from django.contrib.auth.models import UserManager, AbstractBaseUser
from django.contrib.contenttypes.models import ContentType
+from django.contrib.postgres.fields import ArrayField
from django.core import validators
from django.core.exceptions import AppRegistryNotReady
from django.db import models
@@ -34,12 +35,13 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django_pgjson.fields import JsonField
-from djorm_pgarray.fields import TextArrayField
+from django_pglocks import advisory_lock
from taiga.auth.tokens import get_token_for_user
+from taiga.base.utils.colors import generate_random_hex_color
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.files import get_file_path
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING
from taiga.projects.notifications.choices import NotifyLevel
@@ -53,8 +55,8 @@ def get_user_model_safe():
registry not being ready yet.
Raises LookupError if model isn't found.
- Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340
- Ongoing Django issue: https://code.djangoproject.com/ticket/22872
+ Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340
+ Ongoing Django issue: https://code.djangoproject.com/ticket/22872
"""
user_app, user_model = settings.AUTH_USER_MODEL.split('.')
@@ -81,10 +83,6 @@ def get_user_model_safe():
raise
-def generate_random_hex_color():
- return "#{:06x}".format(random.randint(0,0xFFFFFF))
-
-
def get_user_file_path(instance, filename):
return get_file_path(instance, filename, "user")
@@ -198,7 +196,7 @@ class User(AbstractBaseUser, PermissionsMixin):
def _fill_cached_memberships(self):
self._cached_memberships = {}
- qs = self.memberships.prefetch_related("user", "project", "role")
+ qs = self.memberships.select_related("user", "project", "role")
for membership in qs.all():
self._cached_memberships[membership.project.id] = membership
@@ -265,20 +263,21 @@ class User(AbstractBaseUser, PermissionsMixin):
super().save(*args, **kwargs)
def cancel(self):
- self.username = slugify_uniquely("deleted-user", User, slugfield="username")
- self.email = "{}@taiga.io".format(self.username)
- self.is_active = False
- self.full_name = "Deleted user"
- self.color = ""
- self.bio = ""
- self.lang = ""
- self.theme = ""
- self.timezone = ""
- self.colorize_tags = True
- self.token = None
- self.set_unusable_password()
- self.photo = None
- self.save()
+ with advisory_lock("delete-user"):
+ self.username = slugify_uniquely("deleted-user", User, slugfield="username")
+ self.email = "{}@taiga.io".format(self.username)
+ self.is_active = False
+ self.full_name = "Deleted user"
+ self.color = ""
+ self.bio = ""
+ self.lang = ""
+ self.theme = ""
+ self.timezone = ""
+ self.colorize_tags = True
+ self.token = None
+ self.set_unusable_password()
+ self.photo = None
+ self.save()
self.auth_data.all().delete()
# Blocking all owned projects
@@ -293,10 +292,8 @@ class Role(models.Model):
verbose_name=_("name"))
slug = models.SlugField(max_length=250, null=False, blank=True,
verbose_name=_("slug"))
- permissions = TextArrayField(blank=True, null=True,
- default=[],
- verbose_name=_("permissions"),
- choices=MEMBERS_PERMISSIONS)
+ permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS),
+ null=True, blank=True, default=[], verbose_name=_("permissions"))
order = models.IntegerField(default=10, null=False, blank=False,
verbose_name=_("order"))
# null=True is for make work django 1.7 migrations. project
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 97aeafca..a720e46e 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -17,75 +17,56 @@
# along with this program. If not, see .
from django.conf import settings
-from django.core import validators
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
-from taiga.base.fields import PgArrayField, TagsField
+from taiga.base.fields import Field, MethodField, I18NField
+
from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.projects.models import Project
-from .models import User, Role
-from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
-
-from collections import namedtuple
-
-import re
+from .services import get_user_photo_url, get_user_big_photo_url
+from taiga.users.gravatar import get_user_gravatar_id
+from taiga.users.models import User
######################################################
-## User
+# User
######################################################
-class ContactProjectDetailSerializer(serializers.ModelSerializer):
- class Meta:
- model = Project
- fields = ("id", "slug", "name")
+class ContactProjectDetailSerializer(serializers.LightSerializer):
+ id = Field()
+ slug = Field()
+ name = Field()
-class UserSerializer(serializers.ModelSerializer):
- full_name_display = serializers.SerializerMethodField("get_full_name_display")
- photo = serializers.SerializerMethodField("get_photo")
- big_photo = serializers.SerializerMethodField("get_big_photo")
- roles = serializers.SerializerMethodField("get_roles")
- projects_with_me = serializers.SerializerMethodField("get_projects_with_me")
-
- class Meta:
- model = User
- # IMPORTANT: Maintain the UserAdminSerializer Meta up to date
- # with this info (including there the email)
- fields = ("id", "username", "full_name", "full_name_display",
- "color", "bio", "lang", "theme", "timezone", "is_active",
- "photo", "big_photo", "roles", "projects_with_me")
- read_only_fields = ("id",)
-
- def validate_username(self, attrs, source):
- value = attrs[source]
- validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
- _("invalid"))
-
- try:
- validator(value)
- except ValidationError:
- raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, "
- "numbers and /./-/_ characters'"))
-
- if (self.object and
- self.object.username != value and
- User.objects.filter(username=value).exists()):
- raise serializers.ValidationError(_("Invalid username. Try with a different one."))
-
- return attrs
+class UserSerializer(serializers.LightSerializer):
+ id = Field()
+ username = Field()
+ full_name = Field()
+ full_name_display = MethodField()
+ color = Field()
+ bio = Field()
+ lang = Field()
+ theme = Field()
+ timezone = Field()
+ is_active = Field()
+ photo = MethodField()
+ big_photo = MethodField()
+ gravatar_id = MethodField()
+ roles = MethodField()
+ projects_with_me = MethodField()
def get_full_name_display(self, obj):
return obj.get_full_name() if obj else ""
def get_photo(self, user):
- return get_photo_or_gravatar_url(user)
+ return get_user_photo_url(user)
def get_big_photo(self, user):
- return get_big_photo_or_gravatar_url(user)
+ return get_user_big_photo_url(user)
+
+ def get_gravatar_id(self, user):
+ return get_user_gravatar_id(user)
def get_roles(self, user):
return user.memberships. order_by("role__name").values_list("role__name", flat=True).distinct()
@@ -104,25 +85,15 @@ class UserSerializer(serializers.ModelSerializer):
projects = Project.objects.filter(id__in=project_ids)
return ContactProjectDetailSerializer(projects, many=True).data
+
class UserAdminSerializer(UserSerializer):
- total_private_projects = serializers.SerializerMethodField("get_total_private_projects")
- total_public_projects = serializers.SerializerMethodField("get_total_public_projects")
-
- 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",
- "max_private_projects", "max_public_projects",
- "max_memberships_private_projects", "max_memberships_public_projects",
- "total_private_projects", "total_public_projects")
-
- read_only_fields = ("id", "email",
- "max_private_projects", "max_public_projects",
- "max_memberships_private_projects",
- "max_memberships_public_projects")
+ total_private_projects = MethodField()
+ total_public_projects = MethodField()
+ email = Field()
+ max_private_projects = Field()
+ max_public_projects = Field()
+ max_memberships_private_projects = Field()
+ max_memberships_public_projects = Field()
def get_total_private_projects(self, user):
return user.owned_projects.filter(is_private=True).count()
@@ -131,82 +102,83 @@ class UserAdminSerializer(UserSerializer):
return user.owned_projects.filter(is_private=False).count()
-class UserBasicInfoSerializer(UserSerializer):
- class Meta:
- model = User
- fields = ("username", "full_name_display","photo", "big_photo", "is_active", "id")
+class UserBasicInfoSerializer(serializers.LightSerializer):
+ username = Field()
+ full_name_display = MethodField()
+ photo = MethodField()
+ big_photo = MethodField()
+ gravatar_id = MethodField()
+ is_active = Field()
+ id = Field()
+ def get_full_name_display(self, obj):
+ return obj.get_full_name()
-class RecoverySerializer(serializers.Serializer):
- token = serializers.CharField(max_length=200)
- password = serializers.CharField(min_length=6)
+ def get_photo(self, obj):
+ return get_user_photo_url(obj)
+ def get_big_photo(self, obj):
+ return get_user_big_photo_url(obj)
-class ChangeEmailSerializer(serializers.Serializer):
- email_token = serializers.CharField(max_length=200)
+ def get_gravatar_id(self, obj):
+ return get_user_gravatar_id(obj)
+ def to_value(self, instance):
+ if instance is None:
+ return None
-class CancelAccountSerializer(serializers.Serializer):
- cancel_token = serializers.CharField(max_length=200)
+ return super().to_value(instance)
######################################################
-## Role
+# Role
######################################################
-class RoleSerializer(serializers.ModelSerializer):
- members_count = serializers.SerializerMethodField("get_members_count")
- permissions = PgArrayField(required=False)
-
- class Meta:
- model = Role
- fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count')
- i18n_fields = ("name",)
+class RoleSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ project = Field(attr="project_id")
+ order = Field()
+ computable = Field()
+ permissions = Field()
+ members_count = MethodField()
def get_members_count(self, obj):
return obj.memberships.count()
-class ProjectRoleSerializer(serializers.ModelSerializer):
- class Meta:
- model = Role
- fields = ('id', 'name', 'slug', 'order', 'computable')
- i18n_fields = ("name",)
-
-
######################################################
-## Like
+# Like
######################################################
+class HighLightedContentSerializer(serializers.LightSerializer):
+ type = Field()
+ id = Field()
+ ref = Field()
+ slug = Field()
+ name = Field()
+ subject = Field()
+ description = MethodField()
+ assigned_to = Field()
+ status = Field()
+ status_color = Field()
+ tags_colors = MethodField()
+ created_date = Field()
+ is_private = MethodField()
+ logo_small_url = MethodField()
-class HighLightedContentSerializer(serializers.Serializer):
- type = serializers.CharField()
- id = serializers.IntegerField()
- ref = serializers.IntegerField()
- slug = serializers.CharField()
- name = serializers.CharField()
- subject = serializers.CharField()
- description = serializers.SerializerMethodField("get_description")
- assigned_to = serializers.IntegerField()
- status = serializers.CharField()
- status_color = serializers.CharField()
- tags_colors = serializers.SerializerMethodField("get_tags_color")
- created_date = serializers.DateTimeField()
- is_private = serializers.SerializerMethodField("get_is_private")
- logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
+ project = MethodField()
+ project_name = MethodField()
+ project_slug = MethodField()
+ project_is_private = MethodField()
+ project_blocked_code = Field()
- project = serializers.SerializerMethodField("get_project")
- project_name = serializers.SerializerMethodField("get_project_name")
- project_slug = serializers.SerializerMethodField("get_project_slug")
- project_is_private = serializers.SerializerMethodField("get_project_is_private")
- project_blocked_code = serializers.CharField()
+ assigned_to = Field(attr="assigned_to_id")
+ assigned_to_extra_info = MethodField()
- assigned_to_username = serializers.CharField()
- assigned_to_full_name = serializers.CharField()
- assigned_to_photo = serializers.SerializerMethodField("get_photo")
-
- is_watcher = serializers.SerializerMethodField("get_is_watcher")
- total_watchers = serializers.IntegerField()
+ is_watcher = MethodField()
+ total_watchers = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@@ -216,18 +188,18 @@ class HighLightedContentSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
def _none_if_project(self, obj, property):
- type = obj.get("type", "")
+ type = getattr(obj, "type", "")
if type == "project":
return None
- return obj.get(property)
+ return getattr(obj, property)
def _none_if_not_project(self, obj, property):
- type = obj.get("type", "")
+ type = getattr(obj, "type", "")
if type != "project":
return None
- return obj.get(property)
+ return getattr(obj, property)
def get_project(self, obj):
return self._none_if_project(obj, "project")
@@ -253,51 +225,48 @@ class HighLightedContentSerializer(serializers.Serializer):
return get_thumbnail_url(logo, settings.THN_LOGO_SMALL)
return None
- def get_photo(self, obj):
- type = obj.get("type", "")
- if type == "project":
- return None
+ def get_assigned_to_extra_info(self, obj):
+ assigned_to = None
+ if obj.assigned_to_extra_info is not None:
+ assigned_to = User(**obj.assigned_to_extra_info)
+ return UserBasicInfoSerializer(assigned_to).data
- UserData = namedtuple("UserData", ["photo", "email"])
- user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "")
- return get_photo_or_gravatar_url(user_data)
-
- def get_tags_color(self, obj):
- tags = obj.get("tags", [])
+ def get_tags_colors(self, obj):
+ tags = getattr(obj, "tags", [])
tags = tags if tags is not None else []
- tags_colors = obj.get("tags_colors", [])
+ tags_colors = getattr(obj, "tags_colors", [])
tags_colors = tags_colors if tags_colors is not None else []
return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags]
def get_is_watcher(self, obj):
- return obj["id"] in self.user_watching.get(obj["type"], [])
+ return obj.id in self.user_watching.get(obj.type, [])
class LikedObjectSerializer(HighLightedContentSerializer):
- is_fan = serializers.SerializerMethodField("get_is_fan")
- total_fans = serializers.IntegerField()
+ is_fan = MethodField()
+ total_fans = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
- self.user_likes = kwargs.pop("user_likes", {})
+ self.user_likes = kwargs.pop("user_likes", {})
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
def get_is_fan(self, obj):
- return obj["id"] in self.user_likes.get(obj["type"], [])
+ return obj.id in self.user_likes.get(obj.type, [])
class VotedObjectSerializer(HighLightedContentSerializer):
- is_voter = serializers.SerializerMethodField("get_is_voter")
- total_voters = serializers.IntegerField()
+ is_voter = MethodField()
+ total_voters = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
- self.user_votes = kwargs.pop("user_votes", {})
+ self.user_votes = kwargs.pop("user_votes", {})
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
def get_is_voter(self, obj):
- return obj["id"] in self.user_votes.get(obj["type"], [])
+ return obj.id in self.user_votes.get(obj.type, [])
diff --git a/taiga/users/services.py b/taiga/users/services.py
index 5397c46b..4b49353e 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -37,9 +37,6 @@ from taiga.base.utils.urls import get_absolute_url
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.services import get_projects_watched
-from .gravatar import get_gravatar_url
-
-
def get_user_by_username_or_email(username_or_email):
user_model = get_user_model()
@@ -57,7 +54,7 @@ def get_user_by_username_or_email(username_or_email):
return user
-def get_and_validate_user(*, username:str, password:str) -> bool:
+def get_and_validate_user(*, username: str, password: str) -> bool:
"""
Check if user with username/email exists and specified
password matchs well with existing user password.
@@ -75,6 +72,8 @@ def get_and_validate_user(*, username:str, password:str) -> bool:
def get_photo_url(photo):
"""Get a photo absolute url and the photo automatically cropped."""
+ if not photo:
+ return None
try:
url = get_thumbnailer(photo)[settings.THN_AVATAR_SMALL].url
return get_absolute_url(url)
@@ -82,15 +81,17 @@ def get_photo_url(photo):
return None
-def get_photo_or_gravatar_url(user):
- """Get the user's photo/gravatar url."""
- if user:
- return get_photo_url(user.photo) if user.photo else get_gravatar_url(user.email)
- return settings.GRAVATAR_DEFAULT_AVATAR
+def get_user_photo_url(user):
+ """Get the user's photo url."""
+ if not user:
+ return None
+ return get_photo_url(user.photo)
def get_big_photo_url(photo):
"""Get a big photo absolute url and the photo automatically cropped."""
+ if not photo:
+ return None
try:
url = get_thumbnailer(photo)[settings.THN_AVATAR_BIG].url
return get_absolute_url(url)
@@ -98,15 +99,11 @@ def get_big_photo_url(photo):
return None
-def get_big_photo_or_gravatar_url(user):
- """Get the user's big photo/gravatar url."""
+def get_user_big_photo_url(user):
+ """Get the user's big photo url."""
if not user:
- return ""
-
- if user.photo:
- return get_big_photo_url(user.photo)
- else:
- return get_gravatar_url(user.email, size=settings.THN_AVATAR_BIG_SIZE)
+ return None
+ return get_big_photo_url(user.photo)
def get_visible_project_ids(from_user, by_user):
@@ -118,17 +115,17 @@ def get_visible_project_ids(from_user, by_user):
# Authenticated
if by_user.is_authenticated():
- #Calculating the projects wich from_user user is member
+ # Calculating the projects wich from_user user is member
by_user_project_ids = by_user.memberships.values_list("project__id", flat=True)
- #Adding to the condition two OR situations:
- #- The from user has a role that allows access to the project
- #- The to user is the owner
+ # Adding to the condition two OR situations:
+ # - The from user has a role that allows access to the project
+ # - The to user is the owner
member_perm_conditions |= \
Q(project__id__in=by_user_project_ids, role__permissions__contains=required_permissions) |\
Q(project__id__in=by_user_project_ids, is_admin=True)
Membership = apps.get_model('projects', 'Membership')
- #Calculating the user memberships adding the permission filter for the by user
+ # Calculating the user memberships adding the permission filter for the by user
memberships_qs = Membership.objects.filter(member_perm_conditions, user=from_user)
project_ids = memberships_qs.values_list("project__id", flat=True)
return project_ids
@@ -140,8 +137,8 @@ def get_stats_for_user(from_user, by_user):
total_num_projects = len(project_ids)
- roles = [_(r) for r in from_user.memberships.filter(project__id__in=project_ids).values_list(
- "role__name", flat=True)]
+ role_names = from_user.memberships.filter(project__id__in=project_ids).values_list("role__name", flat=True)
+ roles = [_(r) for r in role_names]
roles = list(set(roles))
User = apps.get_model('users', 'User')
@@ -213,9 +210,9 @@ def get_watched_content_for_user(user):
list.append(object_id)
user_watches[ct_model] = list
- #Now for projects,
+ # Now for projects,
projects_watched = get_projects_watched(user)
- project_content_type_model=ContentType.objects.get(app_label="projects", model="project").model
+ project_content_type_model = ContentType.objects.get(app_label="projects", model="project").model
user_watches[project_content_type_model] = projects_watched.values_list("id", flat=True)
return user_watches
@@ -223,22 +220,22 @@ def get_watched_content_for_user(user):
def _build_watched_sql_for_projects(for_user):
sql = """
- SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type,
+ SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type,
tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project,
slug, projects_project.name, null::text AS subject,
notifications_notifypolicy.created_at as created_date,
coalesce(watchers, 0) AS total_watchers, projects_project.total_fans AS total_fans, null::integer AS total_voters,
null::integer AS assigned_to, null::text as status, null::text as status_color
- FROM notifications_notifypolicy
- INNER JOIN projects_project
- ON (projects_project.id = notifications_notifypolicy.project_id)
- LEFT JOIN (SELECT project_id, count(*) watchers
+ FROM notifications_notifypolicy
+ INNER JOIN projects_project
+ ON (projects_project.id = notifications_notifypolicy.project_id)
+ LEFT JOIN (SELECT project_id, count(*) watchers
FROM notifications_notifypolicy
WHERE notifications_notifypolicy.notify_level != {none_notify_level}
GROUP BY project_id
) type_watchers
- ON projects_project.id = type_watchers.project_id
- WHERE
+ ON projects_project.id = type_watchers.project_id
+ WHERE
notifications_notifypolicy.user_id = {for_user_id}
AND notifications_notifypolicy.notify_level != {none_notify_level}
"""
@@ -251,22 +248,22 @@ def _build_watched_sql_for_projects(for_user):
def _build_liked_sql_for_projects(for_user):
sql = """
- SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type,
+ SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type,
tags, likes_like.object_id AS object_id, projects_project.id AS project,
slug, projects_project.name, null::text AS subject,
likes_like.created_date,
coalesce(watchers, 0) AS total_watchers, projects_project.total_fans AS total_fans,
null::integer AS assigned_to, null::text as status, null::text as status_color
- FROM likes_like
- INNER JOIN projects_project
- ON (projects_project.id = likes_like.object_id)
- LEFT JOIN (SELECT project_id, count(*) watchers
+ FROM likes_like
+ INNER JOIN projects_project
+ ON (projects_project.id = likes_like.object_id)
+ LEFT JOIN (SELECT project_id, count(*) watchers
FROM notifications_notifypolicy
WHERE notifications_notifypolicy.notify_level != {none_notify_level}
GROUP BY project_id
) type_watchers
- ON projects_project.id = type_watchers.project_id
- WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id
+ ON projects_project.id = type_watchers.project_id
+ WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id
"""
sql = sql.format(
for_user_id=for_user.id,
@@ -277,39 +274,38 @@ def _build_liked_sql_for_projects(for_user):
def _build_sql_for_type(for_user, type, table_name, action_table, ref_column="ref",
- project_column="project_id", assigned_to_column="assigned_to_id",
- slug_column="slug", subject_column="subject"):
+ project_column="project_id", assigned_to_column="assigned_to_id",
+ slug_column="slug", subject_column="subject"):
sql = """
- SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type,
+ SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type,
tags, {action_table}.object_id AS object_id, {table_name}.{project_column} AS project,
{slug_column} AS slug, null AS name, {subject_column} AS subject,
{action_table}.created_date,
coalesce(watchers, 0) AS total_watchers, null::integer AS total_fans, coalesce(votes_votes.count, 0) AS total_voters,
{assigned_to_column} AS assigned_to, projects_{type}status.name as status, projects_{type}status.color as status_color
- FROM {action_table}
- INNER JOIN django_content_type
- ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
- INNER JOIN {table_name}
- ON ({table_name}.id = {action_table}.object_id)
+ FROM {action_table}
+ INNER JOIN django_content_type
+ ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
+ INNER JOIN {table_name}
+ ON ({table_name}.id = {action_table}.object_id)
INNER JOIN projects_{type}status
- ON (projects_{type}status.id = {table_name}.status_id)
- LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
- ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
- LEFT JOIN votes_votes
- ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
- WHERE {action_table}.user_id = {for_user_id}
+ ON (projects_{type}status.id = {table_name}.status_id)
+ LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
+ ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
+ LEFT JOIN votes_votes
+ ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
+ WHERE {action_table}.user_id = {for_user_id}
"""
sql = sql.format(for_user_id=for_user.id, type=type, table_name=table_name,
- action_table=action_table, ref_column = ref_column,
- project_column=project_column, assigned_to_column=assigned_to_column,
- slug_column=slug_column, subject_column=subject_column)
+ action_table=action_table, ref_column=ref_column,
+ project_column=project_column, assigned_to_column=assigned_to_column,
+ slug_column=slug_column, subject_column=subject_column)
return sql
def get_watched_list(for_user, from_user, type=None, q=None):
filters_sql = ""
- and_needed = False
if type:
filters_sql += " AND type = %(type)s "
@@ -325,8 +321,12 @@ def get_watched_list(for_user, from_user, type=None, q=None):
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo,
- users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
+ users_user.id as assigned_to_id,
+ row_to_json(users_user) as assigned_to_extra_info
+
FROM (
+ {epics_sql}
+ UNION
{userstories_sql}
UNION
{tasks_sql}
@@ -367,6 +367,7 @@ def get_watched_list(for_user, from_user, type=None, q=None):
OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'project' AND 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
+ OR (entities.type = 'epic' AND 'view_epic' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
)
))
-- END Permissions checking
@@ -386,6 +387,7 @@ def get_watched_list(for_user, from_user, type=None, q=None):
userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "notifications_watched", slug_column="null"),
tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "notifications_watched", slug_column="null"),
issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "notifications_watched", slug_column="null"),
+ epics_sql=_build_sql_for_type(for_user, "epic", "epics_epic", "notifications_watched", slug_column="null"),
projects_sql=_build_watched_sql_for_projects(for_user))
cursor = connection.cursor()
@@ -404,7 +406,6 @@ def get_watched_list(for_user, from_user, type=None, q=None):
def get_liked_list(for_user, from_user, type=None, q=None):
filters_sql = ""
- and_needed = False
if type:
filters_sql += " AND type = %(type)s "
@@ -420,7 +421,8 @@ def get_liked_list(for_user, from_user, type=None, q=None):
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo,
- users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
+ users_user.id as assigned_to_id,
+ row_to_json(users_user) as assigned_to_extra_info
FROM (
{projects_sql}
) as entities
@@ -487,7 +489,6 @@ def get_liked_list(for_user, from_user, type=None, q=None):
def get_voted_list(for_user, from_user, type=None, q=None):
filters_sql = ""
- and_needed = False
if type:
filters_sql += " AND type = %(type)s "
@@ -503,8 +504,11 @@ def get_voted_list(for_user, from_user, type=None, q=None):
SELECT entities.*,
projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo,
- users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
+ users_user.id as assigned_to_id,
+ row_to_json(users_user) as assigned_to_extra_info
FROM (
+ {epics_sql}
+ UNION
{userstories_sql}
UNION
{tasks_sql}
@@ -542,6 +546,7 @@ def get_voted_list(for_user, from_user, type=None, q=None):
(entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
+ OR (entities.type = 'epic' AND 'view_epic' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
)
))
-- END Permissions checking
@@ -560,7 +565,8 @@ def get_voted_list(for_user, from_user, type=None, q=None):
filters_sql=filters_sql,
userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "votes_vote", slug_column="null"),
tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "votes_vote", slug_column="null"),
- issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "votes_vote", slug_column="null"))
+ issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "votes_vote", slug_column="null"),
+ epics_sql=_build_sql_for_type(for_user, "epic", "epics_epic", "votes_vote", slug_column="null"))
cursor = connection.cursor()
params = {
diff --git a/taiga/users/validators.py b/taiga/users/validators.py
index 477342de..279e6ce4 100644
--- a/taiga/users/validators.py
+++ b/taiga/users/validators.py
@@ -3,7 +3,6 @@
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
# Copyright (C) 2014-2016 Alejandro Alonso
-# Copyright (C) 2014-2016 Anler Hernández
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
@@ -17,17 +16,84 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.utils.translation import ugettext as _
+from django.core import validators as core_validators
+from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import PgArrayField
-from . import models
+from .models import User, Role
+
+import re
-class RoleExistsValidator:
- def validate_role_id(self, attrs, source):
+######################################################
+# User
+######################################################
+
+class UserValidator(validators.ModelValidator):
+ class Meta:
+ model = User
+ fields = ("username", "full_name", "color", "bio", "lang",
+ "theme", "timezone", "is_active")
+
+ def validate_username(self, attrs, source):
value = attrs[source]
- if not models.Role.objects.filter(pk=value).exists():
- msg = _("There's no role with that id")
- raise serializers.ValidationError(msg)
+ validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
+ _("invalid"))
+
+ try:
+ validator(value)
+ except ValidationError:
+ raise ValidationError(_("Required. 255 characters or fewer. Letters, "
+ "numbers and /./-/_ characters'"))
+
+ if (self.object and
+ self.object.username != value and
+ User.objects.filter(username=value).exists()):
+ raise ValidationError(_("Invalid username. Try with a different one."))
+
return attrs
+
+
+class UserAdminValidator(UserValidator):
+ class Meta:
+ model = User
+ # IMPORTANT: Maintain the UserSerializer Meta up to date
+ # with this info (including here the email)
+ fields = ("username", "full_name", "color", "bio", "lang",
+ "theme", "timezone", "is_active", "email")
+
+
+class RecoveryValidator(validators.Validator):
+ token = serializers.CharField(max_length=200)
+ password = serializers.CharField(min_length=6)
+
+
+class ChangeEmailValidator(validators.Validator):
+ email_token = serializers.CharField(max_length=200)
+
+
+class CancelAccountValidator(validators.Validator):
+ cancel_token = serializers.CharField(max_length=200)
+
+
+######################################################
+# Role
+######################################################
+
+class RoleValidator(validators.ModelValidator):
+ permissions = PgArrayField(required=False)
+
+ class Meta:
+ model = Role
+ fields = ('id', 'name', 'permissions', 'computable', 'project', 'order')
+ i18n_fields = ("name",)
+
+
+class ProjectRoleValidator(validators.ModelValidator):
+ class Meta:
+ model = Role
+ fields = ('id', 'name', 'slug', 'order', 'computable')
diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py
index 62575d2b..94c5ea00 100644
--- a/taiga/userstorage/api.py
+++ b/taiga/userstorage/api.py
@@ -17,7 +17,6 @@
# along with this program. If not, see .
from django.utils.translation import ugettext as _
-from django.db import IntegrityError
from taiga.base.api import ModelCrudViewSet
from taiga.base import exceptions as exc
@@ -25,6 +24,7 @@ from taiga.base import exceptions as exc
from . import models
from . import filters
from . import serializers
+from . import validators
from . import permissions
@@ -32,6 +32,7 @@ class StorageEntriesViewSet(ModelCrudViewSet):
model = models.StorageEntry
filter_backends = (filters.StorageEntriesFilterBackend,)
serializer_class = serializers.StorageEntrySerializer
+ validator_class = validators.StorageEntryValidator
permission_classes = [permissions.StorageEntriesPermission]
lookup_field = "key"
@@ -45,9 +46,11 @@ class StorageEntriesViewSet(ModelCrudViewSet):
obj.owner = self.request.user
def create(self, *args, **kwargs):
- try:
- return super().create(*args, **kwargs)
- except IntegrityError:
- key = self.request.DATA.get("key", None)
- raise exc.IntegrityError(_("Duplicate key value violates unique constraint. "
- "Key '{}' already exists.").format(key))
+ key = self.request.DATA.get("key", None)
+ if (key and self.request.user.is_authenticated() and
+ self.request.user.storage_entries.filter(key=key).exists()):
+ raise exc.BadRequest(
+ _("Duplicate key value violates unique constraint. "
+ "Key '{}' already exists.").format(key)
+ )
+ return super().create(*args, **kwargs)
diff --git a/taiga/userstorage/serializers.py b/taiga/userstorage/serializers.py
index 5fd97692..38765f19 100644
--- a/taiga/userstorage/serializers.py
+++ b/taiga/userstorage/serializers.py
@@ -17,15 +17,11 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
-
-from . import models
+from taiga.base.fields import Field
-class StorageEntrySerializer(serializers.ModelSerializer):
- value = JsonField(label="value")
-
- class Meta:
- model = models.StorageEntry
- fields = ("key", "value", "created_date", "modified_date")
- read_only_fields = ("created_date", "modified_date")
+class StorageEntrySerializer(serializers.LightSerializer):
+ key = Field()
+ value = Field()
+ created_date = Field()
+ modified_date = Field()
diff --git a/taiga/base/tags.py b/taiga/userstorage/validators.py
similarity index 75%
rename from taiga/base/tags.py
rename to taiga/userstorage/validators.py
index 0e1cd866..615b88d7 100644
--- a/taiga/base/tags.py
+++ b/taiga/userstorage/validators.py
@@ -3,7 +3,6 @@
# Copyright (C) 2014-2016 Jesús Espino
# Copyright (C) 2014-2016 David Barragán
# Copyright (C) 2014-2016 Alejandro Alonso
-# Copyright (C) 2014-2016 Anler Hernández
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
@@ -17,14 +16,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from taiga.base.api import validators
-from djorm_pgarray.fields import TextArrayField
+from . import models
-class TaggedMixin(models.Model):
- tags = TextArrayField(default=None, verbose_name=_("tags"))
-
+class StorageEntryValidator(validators.ModelValidator):
class Meta:
- abstract = True
+ model = models.StorageEntry
+ fields = ("key", "value")
diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py
index f15021a0..4648a73a 100644
--- a/taiga/webhooks/api.py
+++ b/taiga/webhooks/api.py
@@ -30,6 +30,7 @@ from taiga.base.decorators import detail_route
from . import models
from . import serializers
+from . import validators
from . import permissions
from . import tasks
@@ -37,6 +38,7 @@ from . import tasks
class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Webhook
serializer_class = serializers.WebhookSerializer
+ validator_class = validators.WebhookValidator
permission_classes = (permissions.WebhookPermission,)
filter_backends = (filters.IsProjectAdminFilterBackend,)
filter_fields = ("project",)
diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py
index 86d47cc3..f16cb76c 100644
--- a/taiga/webhooks/permissions.py
+++ b/taiga/webhooks/permissions.py
@@ -19,7 +19,7 @@
from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectAdmin,
AllowAny, PermissionComponent)
-from taiga.permissions.service import is_project_admin
+from taiga.permissions.services import is_project_admin
class IsWebhookProjectAdmin(PermissionComponent):
diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py
index 9eaf1cdf..7f6736f4 100644
--- a/taiga/webhooks/serializers.py
+++ b/taiga/webhooks/serializers.py
@@ -19,90 +19,87 @@
from django.core.exceptions import ObjectDoesNotExist
from taiga.base.api import serializers
-from taiga.base.fields import TagsField, PgArrayField, JsonField
-
+from taiga.base.fields import Field, MethodField
from taiga.front.templatetags.functions import resolve as resolve_front_url
-from taiga.projects.history import models as history_models
-from taiga.projects.issues import models as issue_models
-from taiga.projects.milestones import models as milestone_models
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.services import get_logo_big_thumbnail_url
-from taiga.projects.tasks import models as task_models
-from taiga.projects.userstories import models as us_models
-from taiga.projects.wiki import models as wiki_models
-
-from taiga.users.gravatar import get_gravatar_url
-from taiga.users.services import get_photo_or_gravatar_url
-
-from .models import Webhook, WebhookLog
+from taiga.users.services import get_user_photo_url
+from taiga.users.gravatar import get_user_gravatar_id
########################################################################
-## WebHooks
+# WebHooks
########################################################################
-class WebhookSerializer(serializers.ModelSerializer):
- logs_counter = serializers.SerializerMethodField("get_logs_counter")
- class Meta:
- model = Webhook
+class WebhookSerializer(serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ name = Field()
+ url = Field()
+ key = Field()
+ logs_counter = MethodField()
def get_logs_counter(self, obj):
return obj.logs.count()
-class WebhookLogSerializer(serializers.ModelSerializer):
- request_data = JsonField()
- request_headers = JsonField()
- response_headers = JsonField()
-
- class Meta:
- model = WebhookLog
+class WebhookLogSerializer(serializers.LightSerializer):
+ id = Field()
+ webhook = Field(attr="webhook_id")
+ url = Field()
+ status = Field()
+ request_data = Field()
+ request_headers = Field()
+ response_data = Field()
+ response_headers = Field()
+ duration = Field()
+ created = Field()
########################################################################
-## User
+# User
########################################################################
-class UserSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- permalink = serializers.SerializerMethodField("get_permalink")
- gravatar_url = serializers.SerializerMethodField("get_gravatar_url")
- username = serializers.SerializerMethodField("get_username")
- full_name = serializers.SerializerMethodField("get_full_name")
- photo = serializers.SerializerMethodField("get_photo")
-
- def get_pk(self, obj):
- return obj.pk
+class UserSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ permalink = MethodField()
+ username = MethodField()
+ full_name = MethodField()
+ photo = MethodField()
+ gravatar_id = MethodField()
def get_permalink(self, obj):
return resolve_front_url("user", obj.username)
- def get_gravatar_url(self, obj):
- return get_gravatar_url(obj.email)
-
def get_username(self, obj):
- return obj.get_username
+ return obj.get_username()
def get_full_name(self, obj):
return obj.get_full_name()
def get_photo(self, obj):
- return get_photo_or_gravatar_url(obj)
+ return get_user_photo_url(obj)
+
+ def get_gravatar_id(self, obj):
+ return get_user_gravatar_id(obj)
+
+ def to_value(self, instance):
+ if instance is None:
+ return None
+
+ return super().to_value(instance)
+
########################################################################
-## Project
+# Project
########################################################################
-class ProjectSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- permalink = serializers.SerializerMethodField("get_permalink")
- name = serializers.SerializerMethodField("get_name")
- logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
-
- def get_pk(self, obj):
- return obj.pk
+class ProjectSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ permalink = MethodField()
+ name = MethodField()
+ logo_big_url = MethodField()
def get_permalink(self, obj):
return resolve_front_url("project", obj.slug)
@@ -115,16 +112,15 @@ class ProjectSerializer(serializers.Serializer):
########################################################################
-## History Serializer
+# History Serializer
########################################################################
-class HistoryDiffField(serializers.Field):
- def to_native(self, value):
+class HistoryDiffField(Field):
+ def to_value(self, value):
# Tip: 'value' is the object returned by
# taiga.projects.history.models.HistoryEntry.values_diff()
ret = {}
-
for key, val in value.items():
if key in ["attachments", "custom_attributes", "description_diff"]:
ret[key] = val
@@ -136,21 +132,21 @@ class HistoryDiffField(serializers.Field):
return ret
-class HistoryEntrySerializer(serializers.ModelSerializer):
- diff = HistoryDiffField(source="values_diff")
-
- class Meta:
- model = history_models.HistoryEntry
- exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user",
- "values", "created_at")
+class HistoryEntrySerializer(serializers.LightSerializer):
+ comment = Field()
+ comment_html = Field()
+ delete_comment_date = Field()
+ comment_versions = Field()
+ edit_comment_date = Field()
+ diff = HistoryDiffField(attr="values_diff")
########################################################################
-## _Misc_
+# _Misc_
########################################################################
-class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
- custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
+class CustomAttributesValuesWebhookSerializerMixin(serializers.LightSerializer):
+ custom_attributes_values = MethodField()
def custom_attributes_queryset(self, project):
raise NotImplementedError()
@@ -160,13 +156,13 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
ret = {}
for attr in custom_attributes:
value = values.get(str(attr["id"]), None)
- if value is not None:
+ if value is not None:
ret[attr["name"]] = value
return ret
try:
- values = obj.custom_attributes_values.attributes_values
+ values = obj.custom_attributes_values.attributes_values
custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name')
return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values)
@@ -174,10 +170,10 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
return None
-class RolePointsSerializer(serializers.Serializer):
- role = serializers.SerializerMethodField("get_role")
- name = serializers.SerializerMethodField("get_name")
- value = serializers.SerializerMethodField("get_value")
+class RolePointsSerializer(serializers.LightSerializer):
+ role = MethodField()
+ name = MethodField()
+ value = MethodField()
def get_role(self, obj):
return obj.role.name
@@ -189,16 +185,33 @@ class RolePointsSerializer(serializers.Serializer):
return obj.points.value
-class UserStoryStatusSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- slug = serializers.SerializerMethodField("get_slug")
- color = serializers.SerializerMethodField("get_color")
- is_closed = serializers.SerializerMethodField("get_is_closed")
- is_archived = serializers.SerializerMethodField("get_is_archived")
+class EpicStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
- def get_pk(self, obj):
- return obj.pk
+ def get_name(self, obj):
+ return obj.name
+
+ def get_slug(self, obj):
+ return obj.slug
+
+ def get_color(self, obj):
+ return obj.color
+
+ def get_is_closed(self, obj):
+ return obj.is_closed
+
+
+class UserStoryStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
+ is_archived = MethodField()
def get_name(self, obj):
return obj.name
@@ -216,15 +229,12 @@ class UserStoryStatusSerializer(serializers.Serializer):
return obj.is_archived
-class TaskStatusSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- slug = serializers.SerializerMethodField("get_slug")
- color = serializers.SerializerMethodField("get_color")
- is_closed = serializers.SerializerMethodField("get_is_closed")
-
- def get_pk(self, obj):
- return obj.pk
+class TaskStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
def get_name(self, obj):
return obj.name
@@ -239,15 +249,12 @@ class TaskStatusSerializer(serializers.Serializer):
return obj.is_closed
-class IssueStatusSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- slug = serializers.SerializerMethodField("get_slug")
- color = serializers.SerializerMethodField("get_color")
- is_closed = serializers.SerializerMethodField("get_is_closed")
-
- def get_pk(self, obj):
- return obj.pk
+class IssueStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
def get_name(self, obj):
return obj.name
@@ -262,13 +269,10 @@ class IssueStatusSerializer(serializers.Serializer):
return obj.is_closed
-class IssueTypeSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- color = serializers.SerializerMethodField("get_color")
-
- def get_pk(self, obj):
- return obj.pk
+class IssueTypeSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ color = MethodField()
def get_name(self, obj):
return obj.name
@@ -277,13 +281,10 @@ class IssueTypeSerializer(serializers.Serializer):
return obj.color
-class PrioritySerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- color = serializers.SerializerMethodField("get_color")
-
- def get_pk(self, obj):
- return obj.pk
+class PrioritySerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ color = MethodField()
def get_name(self, obj):
return obj.name
@@ -292,13 +293,10 @@ class PrioritySerializer(serializers.Serializer):
return obj.color
-class SeveritySerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- color = serializers.SerializerMethodField("get_color")
-
- def get_pk(self, obj):
- return obj.pk
+class SeveritySerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ color = MethodField()
def get_name(self, obj):
return obj.name
@@ -308,57 +306,96 @@ class SeveritySerializer(serializers.Serializer):
########################################################################
-## Milestone
+# Milestone
########################################################################
-class MilestoneSerializer(serializers.ModelSerializer):
+class MilestoneSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ estimated_start = Field()
+ estimated_finish = Field()
+ created_date = Field()
+ modified_date = Field()
+ closed = Field()
+ disponibility = Field()
permalink = serializers.SerializerMethodField("get_permalink")
project = ProjectSerializer()
owner = UserSerializer()
- class Meta:
- model = milestone_models.Milestone
- exclude = ("order", "watchers")
-
def get_permalink(self, obj):
return resolve_front_url("taskboard", obj.project.slug, obj.slug)
+ def to_value(self, instance):
+ if instance is None:
+ return None
+
+ return super().to_value(instance)
+
########################################################################
-## User Story
+# User Story
########################################################################
-class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- permalink = serializers.SerializerMethodField("get_permalink")
- tags = TagsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
+class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
project = ProjectSerializer()
+ is_closed = Field()
+ created_date = Field()
+ modified_date = Field()
+ finish_date = Field()
+ subject = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ generated_from_issue = Field(attr="generated_from_issue_id")
+ external_reference = Field()
+ tribe_gig = Field()
+ watchers = MethodField()
+ is_blocked = Field()
+ blocked_note = Field()
+ tags = Field()
+ permalink = serializers.SerializerMethodField("get_permalink")
owner = UserSerializer()
assigned_to = UserSerializer()
- points = RolePointsSerializer(source="role_points", many=True)
+ points = MethodField()
status = UserStoryStatusSerializer()
milestone = MilestoneSerializer()
- class Meta:
- model = us_models.UserStory
- exclude = ("backlog_order", "sprint_order", "kanban_order", "version", "total_watchers", "is_watcher")
-
def get_permalink(self, obj):
return resolve_front_url("userstory", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project):
return project.userstorycustomattributes.all()
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
+ def get_points(self, obj):
+ return RolePointsSerializer(obj.role_points.all(), many=True).data
+
########################################################################
-## Task
+# Task
########################################################################
-class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
+class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ us_order = Field()
+ taskboard_order = Field()
+ is_iocaine = Field()
+ external_reference = Field()
+ watchers = MethodField()
+ is_blocked = Field()
+ blocked_note = Field()
+ description = Field()
+ tags = Field()
permalink = serializers.SerializerMethodField("get_permalink")
- tags = TagsField(default=[], required=False)
project = ProjectSerializer()
owner = UserSerializer()
assigned_to = UserSerializer()
@@ -366,25 +403,32 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatch
user_story = UserStorySerializer()
milestone = MilestoneSerializer()
- class Meta:
- model = task_models.Task
- exclude = ("version", "total_watchers", "is_watcher")
-
def get_permalink(self, obj):
return resolve_front_url("task", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project):
return project.taskcustomattributes.all()
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
########################################################################
-## Issue
+# Issue
########################################################################
-class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
+class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ external_reference = Field()
+ watchers = MethodField()
+ description = Field()
+ tags = Field()
permalink = serializers.SerializerMethodField("get_permalink")
- tags = TagsField(default=[], required=False)
project = ProjectSerializer()
milestone = MilestoneSerializer()
owner = UserSerializer()
@@ -394,30 +438,78 @@ class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatc
priority = PrioritySerializer()
severity = SeveritySerializer()
- class Meta:
- model = issue_models.Issue
- exclude = ("version", "total_watchers", "is_watcher")
-
def get_permalink(self, obj):
return resolve_front_url("issue", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project):
return project.issuecustomattributes.all()
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
########################################################################
-## Wiki Page
+# Wiki Page
########################################################################
-class WikiPageSerializer(serializers.ModelSerializer):
+class WikiPageSerializer(serializers.LightSerializer):
+ id = Field()
+ slug = Field()
+ content = Field()
+ created_date = Field()
+ modified_date = Field()
permalink = serializers.SerializerMethodField("get_permalink")
project = ProjectSerializer()
owner = UserSerializer()
last_modifier = UserSerializer()
- class Meta:
- model = wiki_models.WikiPage
- exclude = ("watchers", "total_watchers", "is_watcher", "version")
-
def get_permalink(self, obj):
return resolve_front_url("wiki", obj.project.slug, obj.slug)
+
+
+########################################################################
+# Epic
+########################################################################
+
+class EpicSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ created_date = Field()
+ modified_date = Field()
+ subject = Field()
+ watchers = MethodField()
+ description = Field()
+ tags = Field()
+ permalink = serializers.SerializerMethodField("get_permalink")
+ project = ProjectSerializer()
+ owner = UserSerializer()
+ assigned_to = UserSerializer()
+ status = EpicStatusSerializer()
+ epics_order = Field()
+ color = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+
+ def get_permalink(self, obj):
+ return resolve_front_url("epic", obj.project.slug, obj.ref)
+
+ def custom_attributes_queryset(self, project):
+ return project.epiccustomattributes.all()
+
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
+
+class EpicRelatedUserStorySerializer(serializers.LightSerializer):
+ id = Field()
+ user_story = MethodField()
+ epic = MethodField()
+ order = Field()
+
+ def get_user_story(self, obj):
+ return UserStorySerializer(obj.user_story).data
+
+ def get_epic(self, obj):
+ return EpicSerializer(obj.epic).data
diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py
index 7990b928..9e9489ab 100644
--- a/taiga/webhooks/tasks.py
+++ b/taiga/webhooks/tasks.py
@@ -25,7 +25,8 @@ from taiga.base.api.renderers import UnicodeJSONRenderer
from taiga.base.utils.db import get_typename_for_model_instance
from taiga.celery import app
-from .serializers import (UserStorySerializer, IssueSerializer, TaskSerializer,
+from .serializers import (EpicSerializer, EpicRelatedUserStorySerializer,
+ UserStorySerializer, IssueSerializer, TaskSerializer,
WikiPageSerializer, MilestoneSerializer,
HistoryEntrySerializer, UserSerializer)
from .models import WebhookLog
@@ -33,8 +34,11 @@ from .models import WebhookLog
def _serialize(obj):
content_type = get_typename_for_model_instance(obj)
-
- if content_type == "userstories.userstory":
+ if content_type == "epics.epic":
+ return EpicSerializer(obj).data
+ elif content_type == "epics.relateduserstory":
+ return EpicRelatedUserStorySerializer(obj).data
+ elif content_type == "userstories.userstory":
return UserStorySerializer(obj).data
elif content_type == "issues.issue":
return IssueSerializer(obj).data
@@ -62,7 +66,8 @@ def _send_request(webhook_id, url, key, data):
serialized_data = UnicodeJSONRenderer().render(data)
signature = _generate_signature(serialized_data, key)
headers = {
- "X-TAIGA-WEBHOOK-SIGNATURE": signature,
+ "X-TAIGA-WEBHOOK-SIGNATURE": signature, # For backward compatibility
+ "X-Hub-Signature": "sha1={}".format(signature),
"Content-Type": "application/json"
}
request = requests.Request('POST', url, data=serialized_data, headers=headers)
@@ -149,5 +154,4 @@ def test_webhook(webhook_id, url, key, by, date):
data['by'] = UserSerializer(by).data
data['date'] = date
data['data'] = {"test": "test"}
-
return _send_request(webhook_id, url, key, data)
diff --git a/taiga/webhooks/validators.py b/taiga/webhooks/validators.py
new file mode 100644
index 00000000..b95e2e64
--- /dev/null
+++ b/taiga/webhooks/validators.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.base.api import validators
+
+from .models import Webhook
+
+
+class WebhookValidator(validators.ModelValidator):
+ class Meta:
+ model = Webhook
diff --git a/tests/factories.py b/tests/factories.py
index d28bfadc..50f35122 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -27,7 +27,7 @@ from .utils import DUMMY_BMP_DATA
import factory
-from taiga.permissions.permissions import MEMBERS_PERMISSIONS
+from taiga.permissions.choices import MEMBERS_PERMISSIONS
@@ -57,6 +57,7 @@ class ProjectTemplateFactory(Factory):
slug = settings.DEFAULT_PROJECT_TEMPLATE
description = factory.Sequence(lambda n: "Description {}".format(n))
+ epic_statuses = []
us_statuses = []
points = []
task_statuses = []
@@ -120,6 +121,17 @@ class RolePointsFactory(Factory):
points = factory.SubFactory("tests.factories.PointsFactory")
+class EpicAttachmentFactory(Factory):
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+ owner = factory.SubFactory("tests.factories.UserFactory")
+ content_object = factory.SubFactory("tests.factories.EpicFactory")
+ attached_file = factory.django.FileField(data=b"File contents")
+
+ class Meta:
+ model = "attachments.Attachment"
+ strategy = factory.CREATE_STRATEGY
+
+
class UserStoryAttachmentFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
owner = factory.SubFactory("tests.factories.UserFactory")
@@ -229,36 +241,26 @@ class StorageEntryFactory(Factory):
value = factory.Sequence(lambda n: {"value": "value-{}".format(n)})
-class UserStoryFactory(Factory):
+class EpicFactory(Factory):
class Meta:
- model = "userstories.UserStory"
+ model = "epics.Epic"
strategy = factory.CREATE_STRATEGY
ref = factory.Sequence(lambda n: n)
project = factory.SubFactory("tests.factories.ProjectFactory")
owner = factory.SubFactory("tests.factories.UserFactory")
- subject = factory.Sequence(lambda n: "User Story {}".format(n))
- description = factory.Sequence(lambda n: "User Story {} description".format(n))
- status = factory.SubFactory("tests.factories.UserStoryStatusFactory")
- milestone = factory.SubFactory("tests.factories.MilestoneFactory")
+ subject = factory.Sequence(lambda n: "Epic {}".format(n))
+ description = factory.Sequence(lambda n: "Epic {} description".format(n))
+ status = factory.SubFactory("tests.factories.EpicStatusFactory")
-class UserStoryStatusFactory(Factory):
+class RelatedUserStory(Factory):
class Meta:
- model = "projects.UserStoryStatus"
+ model = "epics.RelatedUserStory"
strategy = factory.CREATE_STRATEGY
- name = factory.Sequence(lambda n: "User Story status {}".format(n))
- project = factory.SubFactory("tests.factories.ProjectFactory")
-
-
-class TaskStatusFactory(Factory):
- class Meta:
- model = "projects.TaskStatus"
- strategy = factory.CREATE_STRATEGY
-
- name = factory.Sequence(lambda n: "Task status {}".format(n))
- project = factory.SubFactory("tests.factories.ProjectFactory")
+ epic = factory.SubFactory("tests.factories.EpicFactory")
+ user_story = factory.SubFactory("tests.factories.UserStoryFactory")
class MilestoneFactory(Factory):
@@ -273,6 +275,37 @@ class MilestoneFactory(Factory):
estimated_finish = factory.LazyAttribute(lambda o: o.estimated_start + timedelta(days=7))
+class UserStoryFactory(Factory):
+ class Meta:
+ model = "userstories.UserStory"
+ strategy = factory.CREATE_STRATEGY
+
+ ref = factory.Sequence(lambda n: n)
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+ owner = factory.SubFactory("tests.factories.UserFactory")
+ subject = factory.Sequence(lambda n: "User Story {}".format(n))
+ description = factory.Sequence(lambda n: "User Story {} description".format(n))
+ status = factory.SubFactory("tests.factories.UserStoryStatusFactory")
+ milestone = factory.SubFactory("tests.factories.MilestoneFactory")
+ tags = factory.Faker("words")
+
+
+class TaskFactory(Factory):
+ class Meta:
+ model = "tasks.Task"
+ strategy = factory.CREATE_STRATEGY
+
+ ref = factory.Sequence(lambda n: n)
+ subject = factory.Sequence(lambda n: "Task {}".format(n))
+ description = factory.Sequence(lambda n: "Task {} description".format(n))
+ owner = factory.SubFactory("tests.factories.UserFactory")
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+ status = factory.SubFactory("tests.factories.TaskStatusFactory")
+ milestone = factory.SubFactory("tests.factories.MilestoneFactory")
+ user_story = factory.SubFactory("tests.factories.UserStoryFactory")
+ tags = factory.Faker("words")
+
+
class IssueFactory(Factory):
class Meta:
model = "issues.Issue"
@@ -288,22 +321,7 @@ class IssueFactory(Factory):
priority = factory.SubFactory("tests.factories.PriorityFactory")
type = factory.SubFactory("tests.factories.IssueTypeFactory")
milestone = factory.SubFactory("tests.factories.MilestoneFactory")
-
-
-class TaskFactory(Factory):
- class Meta:
- model = "tasks.Task"
- strategy = factory.CREATE_STRATEGY
-
- ref = factory.Sequence(lambda n: n)
- subject = factory.Sequence(lambda n: "Task {}".format(n))
- description = factory.Sequence(lambda n: "Task {} description".format(n))
- owner = factory.SubFactory("tests.factories.UserFactory")
- project = factory.SubFactory("tests.factories.ProjectFactory")
- status = factory.SubFactory("tests.factories.TaskStatusFactory")
- milestone = factory.SubFactory("tests.factories.MilestoneFactory")
- user_story = factory.SubFactory("tests.factories.UserStoryFactory")
- tags = []
+ tags = factory.Faker("words")
class WikiPageFactory(Factory):
@@ -328,6 +346,33 @@ class WikiLinkFactory(Factory):
order = factory.Sequence(lambda n: n)
+class EpicStatusFactory(Factory):
+ class Meta:
+ model = "projects.EpicStatus"
+ strategy = factory.CREATE_STRATEGY
+
+ name = factory.Sequence(lambda n: "Epic status {}".format(n))
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+
+
+class UserStoryStatusFactory(Factory):
+ class Meta:
+ model = "projects.UserStoryStatus"
+ strategy = factory.CREATE_STRATEGY
+
+ name = factory.Sequence(lambda n: "User Story status {}".format(n))
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+
+
+class TaskStatusFactory(Factory):
+ class Meta:
+ model = "projects.TaskStatus"
+ strategy = factory.CREATE_STRATEGY
+
+ name = factory.Sequence(lambda n: "Task status {}".format(n))
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+
+
class IssueStatusFactory(Factory):
class Meta:
model = "projects.IssueStatus"
@@ -364,6 +409,16 @@ class IssueTypeFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
+class EpicCustomAttributeFactory(Factory):
+ class Meta:
+ model = "custom_attributes.EpicCustomAttribute"
+ strategy = factory.CREATE_STRATEGY
+
+ name = factory.Sequence(lambda n: "Epic Custom Attribute {}".format(n))
+ description = factory.Sequence(lambda n: "Description for Epic Custom Attribute {}".format(n))
+ project = factory.SubFactory("tests.factories.ProjectFactory")
+
+
class UserStoryCustomAttributeFactory(Factory):
class Meta:
model = "custom_attributes.UserStoryCustomAttribute"
@@ -394,6 +449,15 @@ class IssueCustomAttributeFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
+class EpicCustomAttributesValuesFactory(Factory):
+ class Meta:
+ model = "custom_attributes.EpicCustomAttributesValues"
+ strategy = factory.CREATE_STRATEGY
+
+ attributes_values = {}
+ epic = factory.SubFactory("tests.factories.EpicFactory")
+
+
class UserStoryCustomAttributesValuesFactory(Factory):
class Meta:
model = "custom_attributes.UserStoryCustomAttributesValues"
@@ -606,6 +670,26 @@ def create_userstory(**kwargs):
return UserStoryFactory(**defaults)
+def create_epic(**kwargs):
+ "Create an epic along with its dependencies"
+
+ owner = kwargs.pop("owner", None)
+ if not owner:
+ owner = UserFactory.create()
+
+ project = kwargs.pop("project", None)
+ if project is None:
+ project = ProjectFactory.create(owner=owner)
+
+ defaults = {
+ "project": project,
+ "owner": owner,
+ }
+ defaults.update(kwargs)
+
+ return EpicFactory(**defaults)
+
+
def create_project(**kwargs):
"Create a project along with its dependencies"
defaults = {}
diff --git a/tests/integration/resources_permissions/test_application_tokens_resources.py b/tests/integration/resources_permissions/test_application_tokens_resources.py
index 5f6d27e7..10a880b5 100644
--- a/tests/integration/resources_permissions/test_application_tokens_resources.py
+++ b/tests/integration/resources_permissions/test_application_tokens_resources.py
@@ -1,4 +1,22 @@
# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
from django.core.urlresolvers import reverse
from taiga.base.utils import json
diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py
index 41e79e73..456c96f2 100644
--- a/tests/integration/resources_permissions/test_attachment_resources.py
+++ b/tests/integration/resources_permissions/test_attachment_resources.py
@@ -1,11 +1,29 @@
# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see