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:\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\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\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:\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\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\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:\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\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\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:\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\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\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.\n" msgstr "" +"\n" +"Por favor, Vaya al siguiente link para aceptar o rechazar esta propuesta.\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:\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:\n" -" %(support_url)s\n" -"
\n" -" Ota yhteyttä:\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Postituslista:\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\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\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:\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\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\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:\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\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\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:\n" +" %(support_url)s\n" +"
\n" +" Contact us:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Mailing list:\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.\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\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\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\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\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\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\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\".\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.\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.\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:\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:\n" -" %(support_url)s\n" -"
\n" -" Contacteer ons:\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Mailing lijst:\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\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\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:\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:\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\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\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 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:\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:\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\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\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\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\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)sSprint atualizada\n" +"

Olá %(user)s,
%(changer)s atualizou uma sprint em %(project)s\n" "

Sprint %(name)s

\n" " User Story atualizada\n" -"

Olá %(user)s,
%(changer)s atualizou a user story em %(project)s\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\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\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:\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:\n" -" %(support_url)s\n" -"
\n" -" Свяжитесь с нами:" -"\n" -"
\n" -" %(support_email)s\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" @@ -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\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\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.\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:\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\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\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:\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\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\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:\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\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\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 . + from django.core.urlresolvers import reverse from django.core.files.uploadedfile import SimpleUploadedFile from django.test.client import MULTIPART_CONTENT from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.projects.attachments.serializers import AttachmentSerializer @@ -39,11 +57,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -102,6 +120,20 @@ def data(): return m +@pytest.fixture +def data_epic(data): + m = type("Models", (object,), {}) + m.public_epic = f.EpicFactory(project=data.public_project, ref=20) + m.public_epic_attachment = f.EpicAttachmentFactory(project=data.public_project, content_object=m.public_epic) + m.private_epic1 = f.EpicFactory(project=data.private_project1, ref=21) + m.private_epic1_attachment = f.EpicAttachmentFactory(project=data.private_project1, content_object=m.private_epic1) + m.private_epic2 = f.EpicFactory(project=data.private_project2, ref=22) + m.private_epic2_attachment = f.EpicAttachmentFactory(project=data.private_project2, content_object=m.private_epic2) + m.blocked_epic = f.EpicFactory(project=data.blocked_project, ref=23) + m.blocked_epic_attachment = f.EpicAttachmentFactory(project=data.blocked_project, content_object=m.blocked_epic) + return m + + @pytest.fixture def data_us(data): m = type("Models", (object,), {}) @@ -162,6 +194,30 @@ def data_wiki(data): return m +def test_epic_attachment_retrieve(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + def test_user_story_attachment_retrieve(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) @@ -258,6 +314,41 @@ def test_wiki_attachment_retrieve(client, data, data_wiki): assert results == [401, 403, 403, 200, 200] +def test_epic_attachment_update(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data_epic.public_epic_attachment).data + attachment_data["description"] = "test" + attachment_data = json.dumps(attachment_data) + + results = helper_test_http_method(client, 'put', public_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + def test_user_story_attachment_update(client, data, data_us): public_url = reverse("userstory-attachments-detail", args=[data_us.public_user_story_attachment.pk]) @@ -281,20 +372,20 @@ def test_user_story_attachment_update(client, data, data_us): attachment_data = json.dumps(attachment_data) results = helper_test_http_method(client, "put", public_url, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] results = helper_test_http_method(client, "put", private_url1, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] results = helper_test_http_method(client, "put", private_url2, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] results = helper_test_http_method(client, "put", blocked_url, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] def test_task_attachment_update(client, data, data_task): @@ -318,12 +409,15 @@ def test_task_attachment_update(client, data, data_task): results = helper_test_http_method(client, 'put', public_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] @@ -350,12 +444,15 @@ def test_issue_attachment_update(client, data, data_issue): results = helper_test_http_method(client, 'put', public_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] @@ -382,17 +479,50 @@ def test_wiki_attachment_update(client, data, data_wiki): results = helper_test_http_method(client, 'put', public_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] +def test_epic_attachment_patch(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test"} + attachment_data = json.dumps(attachment_data) + + results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) + assert results == [401, 403, 403, 451, 451] + + def test_user_story_attachment_patch(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) @@ -412,10 +542,13 @@ def test_user_story_attachment_patch(client, data, data_us): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] @@ -439,10 +572,13 @@ def test_task_attachment_patch(client, data, data_task): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] @@ -466,10 +602,13 @@ def test_issue_attachment_patch(client, data, data_issue): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] @@ -492,15 +631,44 @@ def test_wiki_attachment_patch(client, data, data_wiki): attachment_data = json.dumps(attachment_data) results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] +def test_epic_attachment_delete(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + def test_user_story_attachment_delete(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) @@ -516,10 +684,13 @@ def test_user_story_attachment_delete(client, data, data_us): results = helper_test_http_method(client, 'delete', public_url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] @@ -539,10 +710,13 @@ def test_task_attachment_delete(client, data, data_task): results = helper_test_http_method(client, 'delete', public_url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] @@ -562,10 +736,13 @@ def test_issue_attachment_delete(client, data, data_issue): results = helper_test_http_method(client, 'delete', public_url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] @@ -584,15 +761,54 @@ def test_wiki_attachment_delete(client, data, data_wiki): ] results = helper_test_http_method(client, 'delete', public_url, None, [None, data.registered_user]) - assert results == [401, 204] + assert results == [401, 403] + results = helper_test_http_method(client, 'delete', private_url1, None, [None, data.registered_user]) - assert results == [401, 204] + assert results == [401, 403] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] +def test_epic_attachment_create(client, data, data_epic): + url = reverse('epic-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test", + "object_id": data_epic.public_epic_attachment.object_id, + "project": data_epic.public_epic_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + assert results == [401, 403, 403, 201, 201] + + attachment_data = {"description": "test", + "object_id": data_epic.blocked_epic_attachment.object_id, + "project": data_epic.blocked_epic_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + assert results == [401, 403, 403, 451, 451] + + def test_user_story_attachment_create(client, data, data_us): url = reverse('userstory-attachments-list') @@ -722,7 +938,7 @@ def test_wiki_attachment_create(client, data, data_wiki): content_type=MULTIPART_CONTENT, after_each_request=_after_each_request_hook) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] attachment_data = {"description": "test", "object_id": data_wiki.blocked_wiki_attachment.object_id, @@ -738,6 +954,21 @@ def test_wiki_attachment_create(client, data, data_wiki): assert results == [401, 403, 403, 451, 451] +def test_epic_attachment_list(client, data, data_epic): + url = reverse('epic-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)] + + def test_user_story_attachment_list(client, data, data_us): url = reverse('userstory-attachments-list') diff --git a/tests/integration/resources_permissions/test_auth_resources.py b/tests/integration/resources_permissions/test_auth_resources.py index 18684d4e..4604936f 100644 --- a/tests/integration/resources_permissions/test_auth_resources.py +++ b/tests/integration/resources_permissions/test_auth_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_epics_custom_attributes_resource.py b/tests/integration/resources_permissions/test_epics_custom_attributes_resource.py new file mode 100644 index 00000000..c35b93bd --- /dev/null +++ b/tests/integration/resources_permissions/test_epics_custom_attributes_resource.py @@ -0,0 +1,458 @@ +# -*- 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.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects import choices as project_choices +from taiga.projects.custom_attributes import serializers +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS) + +from tests import factories as f +from tests.utils import helper_test_http_method + +import pytest +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project1, + role__permissions=[]) + + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project2, + role__permissions=[]) + + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + m.public_epic_ca = f.EpicCustomAttributeFactory(project=m.public_project) + m.private_epic_ca1 = f.EpicCustomAttributeFactory(project=m.private_project1) + m.private_epic_ca2 = f.EpicCustomAttributeFactory(project=m.private_project2) + m.blocked_epic_ca = f.EpicCustomAttributeFactory(project=m.blocked_project) + + m.public_epic = f.EpicFactory(project=m.public_project, + status__project=m.public_project) + m.private_epic1 = f.EpicFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_epic2 = f.EpicFactory(project=m.private_project2, + status__project=m.private_project2) + m.blocked_epic = f.EpicFactory(project=m.blocked_project, + status__project=m.blocked_project) + + m.public_epic_cav = m.public_epic.custom_attributes_values + m.private_epic_cav1 = m.private_epic1.custom_attributes_values + m.private_epic_cav2 = m.private_epic2.custom_attributes_values + m.blocked_epic_cav = m.blocked_epic.custom_attributes_values + + return m + + +######################################################### +# Epic Custom Attribute +######################################################### + +def test_epic_custom_attribute_retrieve(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_custom_attribute_create(client, data): + public_url = reverse('epic-custom-attributes-list') + private1_url = reverse('epic-custom-attributes-list') + private2_url = reverse('epic-custom-attributes-list') + blocked_url = reverse('epic-custom-attributes-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_ca_data = {"name": "test-new", "project": data.public_project.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', public_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + epic_ca_data = {"name": "test-new", "project": data.private_project1.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', private1_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + epic_ca_data = {"name": "test-new", "project": data.private_project2.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', private2_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + epic_ca_data = {"name": "test-new", "project": data.blocked_project.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', blocked_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_custom_attribute_update(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.public_epic_ca).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', public_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.private_epic_ca1).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', private1_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.private_epic_ca2).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', private2_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.blocked_epic_ca).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', private2_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_custom_attribute_delete(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + + + +def test_epic_custom_attribute_list(client, data): + url = reverse('epic-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 4 + assert response.status_code == 200 + + +def test_epic_custom_attribute_patch(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_custom_attribute_action_bulk_update_order(client, data): + url = reverse('epic-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + +######################################################### +# Epic Custom Attribute +######################################################### + + +def test_epic_custom_attributes_values_retrieve(client, data): + public_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.public_epic.pk}) + private_url1 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic1.pk}) + private_url2 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic2.pk}) + blocked_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_custom_attributes_values_update(client, data): + public_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.public_epic.pk}) + private_url1 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic1.pk}) + private_url2 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic2.pk}) + blocked_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.public_epic_cav).data + epic_data["attributes_values"] = {str(data.public_epic_ca.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.private_epic_cav1).data + epic_data["attributes_values"] = {str(data.private_epic_ca1.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.private_epic_cav2).data + epic_data["attributes_values"] = {str(data.private_epic_ca2.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.blocked_epic_cav).data + epic_data["attributes_values"] = {str(data.blocked_epic_ca.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_custom_attributes_values_patch(client, data): + public_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.public_epic.pk}) + private_url1 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic1.pk}) + private_url2 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic2.pk}) + blocked_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"attributes_values": {str(data.public_epic_ca.pk): "test"}, + "version": data.public_epic.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_epic_ca1.pk): "test"}, + "version": data.private_epic1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_epic_ca2.pk): "test"}, + "version": data.private_epic2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.blocked_epic_ca.pk): "test"}, + "version": data.blocked_epic.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] diff --git a/tests/integration/resources_permissions/test_epics_related_userstories_resources.py b/tests/integration/resources_permissions/test_epics_related_userstories_resources.py new file mode 100644 index 00000000..04dd28c1 --- /dev/null +++ b/tests/integration/resources_permissions/test_epics_related_userstories_resources.py @@ -0,0 +1,407 @@ +# -*- 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 . + +import uuid + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.epics.serializers import EpicRelatedUserStorySerializer +from taiga.projects.epics.models import Epic +from taiga.projects.epics.utils import attach_extra_info as attach_epic_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.occ import OCCResourceMixin + +from tests import factories as f +from tests.utils import helper_test_http_method, reconnect_signals +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher + +from unittest import mock + +import pytest +pytestmark = pytest.mark.django_db + + +def setup_function(function): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + m.public_epic = f.EpicFactory(project=m.public_project, + status__project=m.public_project) + m.public_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.public_epic.id) + + m.private_epic1 = f.EpicFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_epic1 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic1.id) + + m.private_epic2 = f.EpicFactory(project=m.private_project2, + status__project=m.private_project2) + m.private_epic2 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic2.id) + + m.blocked_epic = f.EpicFactory(project=m.blocked_project, + status__project=m.blocked_project) + m.blocked_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.blocked_epic.id) + + + m.public_us = f.UserStoryFactory(project=m.public_project) + m.private_us1 = f.UserStoryFactory(project=m.private_project1) + m.private_us2 = f.UserStoryFactory(project=m.private_project2) + m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + + m.public_related_us = f.RelatedUserStory(epic=m.public_epic, user_story=m.public_us) + m.private_related_us1 = f.RelatedUserStory(epic=m.private_epic1, user_story=m.private_us1) + m.private_related_us2 = f.RelatedUserStory(epic=m.private_epic2, user_story=m.private_us2) + m.blocked_related_us = f.RelatedUserStory(epic=m.blocked_epic, user_story=m.blocked_us) + + m.public_project.default_epic_status = m.public_epic.status + m.public_project.save() + m.private_project1.default_epic_status = m.private_epic1.status + m.private_project1.save() + m.private_project2.default_epic_status = m.private_epic2.status + m.private_project2.save() + m.blocked_project.default_epic_status = m.blocked_epic.status + m.blocked_project.save() + + return m + + +def test_epic_related_userstories_list(client, data): + url = reverse('epics-related-userstories-list', args=[data.public_epic.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + client.login(data.registered_user) + + url = reverse('epics-related-userstories-list', args=[data.private_epic1.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + url = reverse('epics-related-userstories-list', args=[data.private_epic2.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + client.login(data.project_owner) + + url = reverse('epics-related-userstories-list', args=[data.blocked_epic.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + +def test_epic_related_userstories_retrieve(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_related_userstories_create(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "user_story": f.UserStoryFactory(project=data.public_project).id, + "epic": data.public_epic.id + }) + url = reverse('epics-related-userstories-list', args=[data.public_epic.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 400] + + create_data = json.dumps({ + "user_story": f.UserStoryFactory(project=data.private_project1).id, + "epic": data.private_epic1.id + }) + url = reverse('epics-related-userstories-list', args=[data.private_epic1.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 400] + + create_data = json.dumps({ + "user_story": f.UserStoryFactory(project=data.private_project2).id, + "epic": data.private_epic2.id + }) + url = reverse('epics-related-userstories-list', args=[data.private_epic2.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 400] + + create_data = json.dumps({ + "user_story": f.UserStoryFactory(project=data.blocked_project).id, + "epic": data.blocked_epic.id + }) + url = reverse('epics-related-userstories-list', args=[data.blocked_epic.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_related_userstories_put_update(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.public_related_us).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', public_url, epic_related_us_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.private_related_us1).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', private_url1, epic_related_us_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.private_related_us2).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', private_url2, epic_related_us_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.blocked_related_us).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_related_us_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_related_userstories_patch_update(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"order": 33}) + + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_related_userstories_delete(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + +def test_bulk_create_related_userstories(client, data): + public_url = reverse('epics-related-userstories-bulk-create', args=[data.public_epic.pk]) + private_url1 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic1.pk]) + private_url2 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic2.pk]) + blocked_url = reverse('epics-related-userstories-bulk-create', args=[data.blocked_epic.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.public_project.id + }) + results = helper_test_http_method(client, 'post', public_url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.private_project1.id + }) + results = helper_test_http_method(client, 'post', private_url1, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.private_project2.id + }) + results = helper_test_http_method(client, 'post', private_url2, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.blocked_project.id + }) + results = helper_test_http_method(client, 'post', blocked_url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py new file mode 100644 index 00000000..0becda53 --- /dev/null +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -0,0 +1,912 @@ +# -*- 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 . + +import uuid + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.epics.serializers import EpicSerializer +from taiga.projects.epics.models import Epic +from taiga.projects.epics.utils import attach_extra_info as attach_epic_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.occ import OCCResourceMixin + +from tests import factories as f +from tests.utils import helper_test_http_method, reconnect_signals +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher + +from unittest import mock + +import pytest +pytestmark = pytest.mark.django_db + + +def setup_function(function): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + m.public_epic = f.EpicFactory(project=m.public_project, + status__project=m.public_project) + m.public_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.public_epic.id) + + m.private_epic1 = f.EpicFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_epic1 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic1.id) + + m.private_epic2 = f.EpicFactory(project=m.private_project2, + status__project=m.private_project2) + m.private_epic2 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic2.id) + + m.blocked_epic = f.EpicFactory(project=m.blocked_project, + status__project=m.blocked_project) + m.blocked_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.blocked_epic.id) + + + m.public_us = f.UserStoryFactory(project=m.public_project) + m.private_us1 = f.UserStoryFactory(project=m.private_project1) + m.private_us2 = f.UserStoryFactory(project=m.private_project2) + m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + + m.public_related_us = f.RelatedUserStory(epic=m.public_epic, user_story=m.public_us) + m.private_related_us1 = f.RelatedUserStory(epic=m.private_epic1, user_story=m.private_us1) + m.private_related_us2 = f.RelatedUserStory(epic=m.private_epic2, user_story=m.private_us2) + m.blocked_related_us = f.RelatedUserStory(epic=m.blocked_epic, user_story=m.blocked_us) + + m.public_project.default_epic_status = m.public_epic.status + m.public_project.save() + m.private_project1.default_epic_status = m.private_epic1.status + m.private_project1.save() + m.private_project2.default_epic_status = m.private_epic2.status + m.private_project2.save() + m.blocked_project.default_epic_status = m.blocked_epic.status + m.blocked_project.save() + + return m + + +def test_epic_list(client, data): + url = reverse('epics-list') + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 4 + assert response.status_code == 200 + + +def test_epic_retrieve(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_create(client, data): + url = reverse('epics-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "status": data.public_project.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "status": data.private_project1.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "status": data.private_project2.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "status": data.blocked_project.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_update(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + epic_data = EpicSerializer(data.public_epic).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic1).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic2).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.blocked_epic).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + epic_data = EpicSerializer(data.public_epic).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic1).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic2).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.blocked_epic).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_update_and_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + epic_data = EpicSerializer(data.public_epic).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic1).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic2).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.blocked_epic).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + epic_status1 = f.EpicStatusFactory.create(project=project1) + epic_status2 = f.EpicStatusFactory.create(project=project2) + + project1.default_epic_status = epic_status1 + project2.default_epic_status = epic_status2 + + project1.save() + project2.save() + + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + epic = f.EpicFactory.create(project=project1) + epic = attach_epic_extra_info(Epic.objects.all()).get(id=epic.id) + + url = reverse('epics-detail', kwargs={"pk": epic.pk}) + + # Test user with permissions in both projects + client.login(user1) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 200 + + epic.project = project1 + epic.save() + + # Test user with permissions in only origin project + client.login(user2) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 403 + + epic.project = project1 + epic.save() + + # Test user with permissions in only destionation project + client.login(user3) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 403 + + epic.project = project1 + epic.save() + + # Test user without permissions in the projects + client.login(user4) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 403 + + epic.project = project1 + epic.save() + + +def test_epic_patch_update(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"subject": "test", "version": data.public_epic.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_epic1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_epic2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.blocked_epic.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_patch_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_epic.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_epic1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_epic2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_epic.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_patch_update_and_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_epic.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_epic1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_epic2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_epic.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_delete(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + +def test_epic_action_bulk_create(client, data): + url = reverse('epics-bulk-create') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.public_epic.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.private_epic1.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.private_epic2.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.blocked_epic.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_action_upvote(client, data): + public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-upvote', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-upvote', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_action_downvote(client, data): + public_url = reverse('epics-downvote', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-downvote', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-downvote', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-downvote', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_voters_list(client, data): + public_url = reverse('epic-voters-list', kwargs={"resource_id": data.public_epic.pk}) + private_url1 = reverse('epic-voters-list', kwargs={"resource_id": data.private_epic1.pk}) + private_url2 = reverse('epic-voters-list', kwargs={"resource_id": data.private_epic2.pk}) + blocked_url = reverse('epic-voters-list', kwargs={"resource_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_voters_retrieve(client, data): + add_vote(data.public_epic, data.project_owner) + public_url = reverse('epic-voters-detail', kwargs={"resource_id": data.public_epic.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_epic1, data.project_owner) + private_url1 = reverse('epic-voters-detail', kwargs={"resource_id": data.private_epic1.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_epic2, data.project_owner) + private_url2 = reverse('epic-voters-detail', kwargs={"resource_id": data.private_epic2.pk, + "pk": data.project_owner.pk}) + + add_vote(data.blocked_epic, data.project_owner) + blocked_url = reverse('epic-voters-detail', kwargs={"resource_id": data.blocked_epic.pk, + "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_action_watch(client, data): + public_url = reverse('epics-watch', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-watch', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-watch', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-watch', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_action_unwatch(client, data): + public_url = reverse('epics-unwatch', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-unwatch', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-unwatch', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-unwatch', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_watchers_list(client, data): + public_url = reverse('epic-watchers-list', kwargs={"resource_id": data.public_epic.pk}) + private_url1 = reverse('epic-watchers-list', kwargs={"resource_id": data.private_epic1.pk}) + private_url2 = reverse('epic-watchers-list', kwargs={"resource_id": data.private_epic2.pk}) + blocked_url = reverse('epic-watchers-list', kwargs={"resource_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_watchers_retrieve(client, data): + add_watcher(data.public_epic, data.project_owner) + public_url = reverse('epic-watchers-detail', kwargs={"resource_id": data.public_epic.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_epic1, data.project_owner) + private_url1 = reverse('epic-watchers-detail', kwargs={"resource_id": data.private_epic1.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_epic2, data.project_owner) + private_url2 = reverse('epic-watchers-detail', kwargs={"resource_id": data.private_epic2.pk, + "pk": data.project_owner.pk}) + + add_watcher(data.blocked_epic, data.project_owner) + blocked_url = reverse('epic-watchers-detail', kwargs={"resource_id": data.blocked_epic.pk, + "pk": data.project_owner.pk}) + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epics_csv(client, data): + url = reverse('epics-csv') + csv_public_uuid = data.public_project.epics_csv_uuid + csv_private1_uuid = data.private_project1.epics_csv_uuid + csv_private2_uuid = data.private_project1.epics_csv_uuid + csv_blocked_uuid = data.blocked_project.epics_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_feedback.py b/tests/integration/resources_permissions/test_feedback.py index 650f6b07..752c1e71 100644 --- a/tests/integration/resources_permissions/test_feedback.py +++ b/tests/integration/resources_permissions/test_feedback.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 tests import factories as f diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index daf7fa67..95d77928 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -1,7 +1,30 @@ # -*- coding: utf-8 -*- -from django.core.urlresolvers import reverse +# 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.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from django.core.urlresolvers import reverse +from django.utils import timezone + +from taiga.base.utils import json +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.history.models import HistoryEntry +from taiga.projects.history.choices import HistoryType +from taiga.projects.history.services import make_key_from_model_object from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -22,19 +45,19 @@ def teardown_module(module): def data(): m = type("Models", (object,), {}) - m.registered_user = f.UserFactory.create() - m.project_member_with_perms = f.UserFactory.create() - m.project_member_without_perms = f.UserFactory.create() - m.project_owner = f.UserFactory.create() - m.other_user = f.UserFactory.create() + m.registered_user = f.UserFactory.create(full_name="registered_user") + m.project_member_with_perms = f.UserFactory.create(full_name="project_member_with_perms") + m.project_member_without_perms = f.UserFactory.create(full_name="project_member_without_perms") + m.project_owner = f.UserFactory.create(full_name="project_owner") + m.other_user = f.UserFactory.create(full_name="other_user") m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -77,39 +100,274 @@ def data(): return m +######################################################### +## Epics +######################################################### + +@pytest.fixture +def data_epic(data): + m = type("Models", (object,), {}) + m.public_epic = f.EpicFactory(project=data.public_project, ref=22) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, + comment="testing public", + key=make_key_from_model_object(m.public_epic), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_epic1 = f.EpicFactory(project=data.private_project1, ref=26) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, + comment="testing 1", + key=make_key_from_model_object(m.private_epic1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_epic2 = f.EpicFactory(project=data.private_project2, ref=210) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, + comment="testing 2", + key=make_key_from_model_object(m.private_epic2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + +def test_epic_history_retrieve(client, data, data_epic): + public_url = reverse('epic-history-detail', kwargs={"pk": data_epic.public_epic.pk}) + private_url1 = reverse('epic-history-detail', kwargs={"pk": data_epic.private_epic1.pk}) + private_url2 = reverse('epic-history-detail', kwargs={"pk": data_epic.private_epic2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_action_edit_comment(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-edit-comment', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-edit-comment', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-edit-comment', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_action_delete_comment(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-delete-comment', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-delete-comment', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-delete-comment', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_epic.public_history_entry.delete_comment_date = None + data_epic.public_history_entry.delete_comment_user = None + data_epic.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry1.delete_comment_date = None + data_epic.private_history_entry1.delete_comment_user = None + data_epic.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry2.delete_comment_date = None + data_epic.private_history_entry2.delete_comment_user = None + data_epic.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_epic_action_undelete_comment(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-undelete-comment', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-undelete-comment', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-undelete-comment', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_epic.public_history_entry.delete_comment_date = timezone.now() + data_epic.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_epic.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry1.delete_comment_date = timezone.now() + data_epic.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_epic.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry2.delete_comment_date = timezone.now() + data_epic.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_epic.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_epic_action_comment_versions(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-comment-versions', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-comment-versions', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-comment-versions', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## User stories +######################################################### + @pytest.fixture def data_us(data): m = type("Models", (object,), {}) m.public_user_story = f.UserStoryFactory(project=data.public_project, ref=1) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, + comment="testing public", + key=make_key_from_model_object(m.public_user_story), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_user_story1 = f.UserStoryFactory(project=data.private_project1, ref=5) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, + comment="testing 1", + key=make_key_from_model_object(m.private_user_story1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9) - return m - - -@pytest.fixture -def data_task(data): - m = type("Models", (object,), {}) - m.public_task = f.TaskFactory(project=data.public_project, ref=2) - m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) - m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) - return m - - -@pytest.fixture -def data_issue(data): - m = type("Models", (object,), {}) - m.public_issue = f.IssueFactory(project=data.public_project, ref=3) - m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) - m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) - return m - - -@pytest.fixture -def data_wiki(data): - m = type("Models", (object,), {}) - m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) - m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) - m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, + comment="testing 2", + key=make_key_from_model_object(m.private_user_story2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) return m @@ -134,6 +392,224 @@ def test_user_story_history_retrieve(client, data, data_us): assert results == [401, 403, 403, 200, 200] +def test_user_story_action_edit_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_action_delete_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_us.public_history_entry.delete_comment_date = None + data_us.public_history_entry.delete_comment_user = None + data_us.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry1.delete_comment_date = None + data_us.private_history_entry1.delete_comment_user = None + data_us.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry2.delete_comment_date = None + data_us.private_history_entry2.delete_comment_user = None + data_us.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_user_story_action_undelete_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_us.public_history_entry.delete_comment_date = timezone.now() + data_us.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry1.delete_comment_date = timezone.now() + data_us.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry2.delete_comment_date = timezone.now() + data_us.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_user_story_action_comment_versions(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Tasks +######################################################### + +@pytest.fixture +def data_task(data): + m = type("Models", (object,), {}) + m.public_task = f.TaskFactory(project=data.public_project, ref=2) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, + comment="testing public", + key=make_key_from_model_object(m.public_task), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, + comment="testing 1", + key=make_key_from_model_object(m.private_task1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, + comment="testing 2", + key=make_key_from_model_object(m.private_task2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_task_history_retrieve(client, data, data_task): public_url = reverse('task-history-detail', kwargs={"pk": data_task.public_task.pk}) private_url1 = reverse('task-history-detail', kwargs={"pk": data_task.private_task1.pk}) @@ -155,6 +631,224 @@ def test_task_history_retrieve(client, data, data_task): assert results == [401, 403, 403, 200, 200] +def test_task_action_edit_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_action_delete_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_task.public_history_entry.delete_comment_date = None + data_task.public_history_entry.delete_comment_user = None + data_task.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry1.delete_comment_date = None + data_task.private_history_entry1.delete_comment_user = None + data_task.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry2.delete_comment_date = None + data_task.private_history_entry2.delete_comment_user = None + data_task.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_task_action_undelete_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_task.public_history_entry.delete_comment_date = timezone.now() + data_task.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry1.delete_comment_date = timezone.now() + data_task.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry2.delete_comment_date = timezone.now() + data_task.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_task_action_comment_versions(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Issues +######################################################### + +@pytest.fixture +def data_issue(data): + m = type("Models", (object,), {}) + m.public_issue = f.IssueFactory(project=data.public_project, ref=3) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, + comment="testing public", + key=make_key_from_model_object(m.public_issue), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, + comment="testing 1", + key=make_key_from_model_object(m.private_issue1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, + comment="testing 2", + key=make_key_from_model_object(m.private_issue2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_issue_history_retrieve(client, data, data_issue): public_url = reverse('issue-history-detail', kwargs={"pk": data_issue.public_issue.pk}) private_url1 = reverse('issue-history-detail', kwargs={"pk": data_issue.private_issue1.pk}) @@ -176,6 +870,224 @@ def test_issue_history_retrieve(client, data, data_issue): assert results == [401, 403, 403, 200, 200] +def test_issue_action_edit_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_action_delete_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_issue.public_history_entry.delete_comment_date = None + data_issue.public_history_entry.delete_comment_user = None + data_issue.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry1.delete_comment_date = None + data_issue.private_history_entry1.delete_comment_user = None + data_issue.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry2.delete_comment_date = None + data_issue.private_history_entry2.delete_comment_user = None + data_issue.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_issue_action_undelete_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_issue.public_history_entry.delete_comment_date = timezone.now() + data_issue.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry1.delete_comment_date = timezone.now() + data_issue.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry2.delete_comment_date = timezone.now() + data_issue.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_issue_action_comment_versions(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Wiki pages +######################################################### + +@pytest.fixture +def data_wiki(data): + m = type("Models", (object,), {}) + m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, + comment="testing public", + key=make_key_from_model_object(m.public_wiki), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, + comment="testing 1", + key=make_key_from_model_object(m.private_wiki1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, + comment="testing 2", + key=make_key_from_model_object(m.private_wiki2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_wiki_history_retrieve(client, data, data_wiki): public_url = reverse('wiki-history-detail', kwargs={"pk": data_wiki.public_wiki.pk}) private_url1 = reverse('wiki-history-detail', kwargs={"pk": data_wiki.private_wiki1.pk}) @@ -195,3 +1107,189 @@ def test_wiki_history_retrieve(client, data, data_wiki): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_wiki_action_edit_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_action_delete_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_wiki.public_history_entry.delete_comment_date = None + data_wiki.public_history_entry.delete_comment_user = None + data_wiki.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry1.delete_comment_date = None + data_wiki.private_history_entry1.delete_comment_user = None + data_wiki.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry2.delete_comment_date = None + data_wiki.private_history_entry2.delete_comment_user = None + data_wiki.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_wiki_action_undelete_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_wiki.public_history_entry.delete_comment_date = timezone.now() + data_wiki.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry1.delete_comment_date = timezone.now() + data_wiki.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry2.delete_comment_date = timezone.now() + data_wiki.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_wiki_action_comment_versions(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index bb77bdc0..1ec28157 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -22,8 +22,8 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, - ANON_PERMISSIONS, USER_PERMISSIONS) +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS) from tests import factories as f from tests.utils import helper_test_http_method @@ -44,11 +44,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 2def80f6..4f93ea0c 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -1,11 +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 +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + import uuid from django.core.urlresolvers import reverse from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.projects.issues.models import Issue from taiga.projects.issues.serializers import IssueSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects.issues.utils import attach_extra_info as attach_issue_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json from tests import factories as f @@ -40,25 +62,32 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -111,28 +140,84 @@ def data(): priority__project=m.public_project, type__project=m.public_project, milestone__project=m.public_project) + m.public_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.public_issue.id) + m.private_issue1 = f.IssueFactory(project=m.private_project1, status__project=m.private_project1, severity__project=m.private_project1, priority__project=m.private_project1, type__project=m.private_project1, milestone__project=m.private_project1) + m.private_issue1 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue1.id) + m.private_issue2 = f.IssueFactory(project=m.private_project2, status__project=m.private_project2, severity__project=m.private_project2, priority__project=m.private_project2, type__project=m.private_project2, milestone__project=m.private_project2) + m.private_issue2 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue2.id) + m.blocked_issue = f.IssueFactory(project=m.blocked_project, status__project=m.blocked_project, severity__project=m.blocked_project, priority__project=m.blocked_project, type__project=m.blocked_project, milestone__project=m.blocked_project) + m.blocked_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.blocked_issue.id) return m +def test_issue_list(client, data): + url = reverse('issues-list') + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 4 + assert response.status_code == 200 + + +def test_issue_list_filter_by_project_ok(client, data): + url = "{}?project={}".format(reverse("issues-list"), data.public_project.pk) + + client.login(data.project_owner) + response = client.get(url) + + assert response.status_code == 200 + assert len(response.data) == 1 + + +def test_issue_list_filter_by_project_error(client, data): + url = "{}?project={}".format(reverse("issues-list"), "-ERROR-") + + client.login(data.project_owner) + response = client.get(url) + + assert response.status_code == 400 + + def test_issue_retrieve(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) @@ -157,7 +242,67 @@ def test_issue_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_issue_update(client, data): +def test_issue_create(client, data): + url = reverse('issues-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "severity": data.public_project.severities.all()[0].pk, + "priority": data.public_project.priorities.all()[0].pk, + "status": data.public_project.issue_statuses.all()[0].pk, + "type": data.public_project.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "severity": data.private_project1.severities.all()[0].pk, + "priority": data.private_project1.priorities.all()[0].pk, + "status": data.private_project1.issue_statuses.all()[0].pk, + "type": data.private_project1.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "severity": data.private_project2.severities.all()[0].pk, + "priority": data.private_project2.priorities.all()[0].pk, + "status": data.private_project2.issue_statuses.all()[0].pk, + "type": data.private_project2.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "severity": data.blocked_project.severities.all()[0].pk, + "priority": data.blocked_project.priorities.all()[0].pk, + "status": data.blocked_project.issue_statuses.all()[0].pk, + "type": data.blocked_project.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_put_update(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) @@ -172,32 +317,116 @@ def test_issue_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - issue_data = IssueSerializer(data.public_issue).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', public_url, issue_data, users) - assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.public_issue).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] - issue_data = IssueSerializer(data.private_issue1).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', private_url1, issue_data, users) - assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.private_issue1).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] - issue_data = IssueSerializer(data.private_issue2).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', private_url2, issue_data, users) - assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.private_issue2).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] - issue_data = IssueSerializer(data.blocked_issue).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) - assert results == [401, 403, 403, 451, 451] + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] -def test_issue_update_with_project_change(client): +def test_issue_put_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + issue_data = IssueSerializer(data.public_issue).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue1).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue2).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_put_update_and_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + issue_data = IssueSerializer(data.public_issue).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue1).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue2).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_put_update_with_project_change(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() user3 = f.UserFactory.create() @@ -232,24 +461,28 @@ def test_issue_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) issue = f.IssueFactory.create(project=project1) + issue = attach_issue_extra_info(Issue.objects.all()).get(id=issue.id) url = reverse('issues-detail', kwargs={"pk": issue.pk}) @@ -310,6 +543,118 @@ def test_issue_update_with_project_change(client): issue.save() +def test_issue_patch_update(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"subject": "test", "version": data.public_issue.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_patch_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_issue.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_issue1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_issue2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_issue.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_patch_update_and_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_issue.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_issue1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_issue2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_issue.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + def test_issue_delete(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) @@ -333,148 +678,7 @@ def test_issue_delete(client, data): assert results == [401, 403, 403, 451] -def test_issue_list(client, data): - url = reverse('issues-list') - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 4 - assert response.status_code == 200 - - -def test_issue_list_filter_by_project_ok(client, data): - url = "{}?project={}".format(reverse("issues-list"), data.public_project.pk) - - client.login(data.project_owner) - response = client.get(url) - - assert response.status_code == 200 - assert len(response.data) == 1 - - -def test_issue_list_filter_by_project_error(client, data): - url = "{}?project={}".format(reverse("issues-list"), "-ERROR-") - - client.login(data.project_owner) - response = client.get(url) - - assert response.status_code == 400 - - -def test_issue_create(client, data): - url = reverse('issues-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({ - "subject": "test", - "ref": 1, - "project": data.public_project.pk, - "severity": data.public_project.severities.all()[0].pk, - "priority": data.public_project.priorities.all()[0].pk, - "status": data.public_project.issue_statuses.all()[0].pk, - "type": data.public_project.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 2, - "project": data.private_project1.pk, - "severity": data.private_project1.severities.all()[0].pk, - "priority": data.private_project1.priorities.all()[0].pk, - "status": data.private_project1.issue_statuses.all()[0].pk, - "type": data.private_project1.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.private_project2.pk, - "severity": data.private_project2.severities.all()[0].pk, - "priority": data.private_project2.priorities.all()[0].pk, - "status": data.private_project2.issue_statuses.all()[0].pk, - "type": data.private_project2.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.blocked_project.pk, - "severity": data.blocked_project.severities.all()[0].pk, - "priority": data.blocked_project.priorities.all()[0].pk, - "status": data.blocked_project.issue_statuses.all()[0].pk, - "type": data.blocked_project.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_issue_patch(client, data): - public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) - private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) - private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) - blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"subject": "test", "version": data.public_issue.version}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_issue_bulk_create(client, data): +def test_issue_action_bulk_create(client, data): data.public_issue.project.default_issue_status = f.IssueStatusFactory() data.public_issue.project.default_issue_type = f.IssueTypeFactory() data.public_issue.project.default_priority = f.PriorityFactory() @@ -512,12 +716,12 @@ def test_issue_bulk_create(client, data): bulk_data = json.dumps({"bulk_issues": "test1\ntest2", "project_id": data.public_issue.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_issues": "test1\ntest2", "project_id": data.private_issue1.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_issues": "test1\ntest2", "project_id": data.private_issue2.project.pk}) @@ -634,34 +838,6 @@ def test_issue_voters_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_issues_csv(client, data): - url = reverse('issues-csv') - csv_public_uuid = data.public_project.issues_csv_uuid - csv_private1_uuid = data.private_project1.issues_csv_uuid - csv_private2_uuid = data.private_project2.issues_csv_uuid - csv_blocked_uuid = data.blocked_project.issues_csv_uuid - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - def test_issue_action_watch(client, data): public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk}) @@ -763,3 +939,31 @@ def test_issue_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_issues_csv(client, data): + url = reverse('issues-csv') + csv_public_uuid = data.public_project.issues_csv_uuid + csv_private1_uuid = data.private_project1.issues_csv_uuid + csv_private2_uuid = data.private_project2.issues_csv_uuid + csv_blocked_uuid = data.blocked_project.issues_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index c8fedd14..1a3ab5cb 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -1,13 +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 +# 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 from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone +from taiga.projects.milestones.utils import attach_extra_info as attach_milestone_extra_info from taiga.projects.notifications.services import add_watcher -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -36,46 +57,57 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, role__project=m.private_project1, role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project2, user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -94,13 +126,17 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) m.public_milestone = f.MilestoneFactory(project=m.public_project) + m.public_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.public_milestone.id) m.private_milestone1 = f.MilestoneFactory(project=m.private_project1) + m.private_milestone1 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone1.id) m.private_milestone2 = f.MilestoneFactory(project=m.private_project2) + m.private_milestone2 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone2.id) m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project) + m.blocked_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.blocked_milestone.id) return m @@ -404,16 +440,16 @@ def test_milestone_watchers_list(client, data): def test_milestone_watchers_retrieve(client, data): add_watcher(data.public_milestone, data.project_owner) public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_milestone1, data.project_owner) private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_milestone2, data.project_owner) private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_milestone, data.project_owner) blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py index a2f8dd98..68f552f4 100644 --- a/tests/integration/resources_permissions/test_modules_resources.py +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -1,9 +1,27 @@ # -*- 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 . + import uuid from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json from tests import factories as f @@ -39,11 +57,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -193,18 +211,18 @@ def test_modules_patch(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 403, 204] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 403, 204] - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 403, 204] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 403, 204] - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [404, 404, 404, 403, 204] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [404, 404, 404, 403, 204] - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [404, 404, 404, 403, 451] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [404, 404, 404, 403, 451] diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index ac929424..504b6a6f 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_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 . + from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects import serializers from taiga.users.serializers import RoleSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method @@ -27,20 +45,24 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=['view_project'], public_permissions=['view_project'], - owner=m.project_owner) + owner=m.project_owner, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=['view_project'], public_permissions=['view_project'], - owner=m.project_owner) + owner=m.project_owner, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) + owner=m.project_owner, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, - blocked_code=project_choices.BLOCKED_BY_STAFF) + blocked_code=project_choices.BLOCKED_BY_STAFF, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -93,6 +115,11 @@ def data(): user=m.project_owner, is_admin=True) + m.public_epic_status = f.EpicStatusFactory(project=m.public_project) + m.private_epic_status1 = f.EpicStatusFactory(project=m.private_project1) + m.private_epic_status2 = f.EpicStatusFactory(project=m.private_project2) + m.blocked_epic_status = f.EpicStatusFactory(project=m.blocked_project) + m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) m.private_points2 = f.PointsFactory(project=m.private_project2) @@ -133,6 +160,10 @@ def data(): return m +##################################################### +# Roles +##################################################### + def test_roles_retrieve(client, data): public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) @@ -277,6 +308,198 @@ def test_roles_patch(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Epic Status +##################################################### + +def test_epic_status_retrieve(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_status_update(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_status_data = serializers.EpicStatusSerializer(data.public_epic_status).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', public_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_status_data = serializers.EpicStatusSerializer(data.private_epic_status1).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', private1_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_status_data = serializers.EpicStatusSerializer(data.private_epic_status2).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', private2_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_status_data = serializers.EpicStatusSerializer(data.blocked_epic_status).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_status_delete(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_status_list(client, data): + url = reverse('epic-statuses-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 4 + assert response.status_code == 200 + + +def test_epic_status_patch(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_status_action_bulk_update_order(client, data): + url = reverse('epic-statuses-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + + +##################################################### +# Points +##################################################### + def test_points_retrieve(client, data): public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) @@ -461,6 +684,10 @@ def test_points_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# User Story Status +##################################################### + def test_user_story_status_retrieve(client, data): public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) @@ -548,7 +775,6 @@ def test_user_story_status_delete(client, data): assert results == [401, 403, 403, 403, 451] - def test_user_story_status_list(client, data): url = reverse('userstory-statuses-list') @@ -646,6 +872,10 @@ def test_user_story_status_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Task Status +##################################################### + def test_task_status_retrieve(client, data): public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) @@ -733,7 +963,6 @@ def test_task_status_delete(client, data): assert results == [401, 403, 403, 403, 451] - def test_task_status_list(client, data): url = reverse('task-statuses-list') @@ -831,6 +1060,10 @@ def test_task_status_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Issue Status +##################################################### + def test_issue_status_retrieve(client, data): public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) @@ -1015,6 +1248,10 @@ def test_issue_status_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Issue Type +##################################################### + def test_issue_type_retrieve(client, data): public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) @@ -1199,6 +1436,10 @@ def test_issue_type_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Priority +##################################################### + def test_priority_retrieve(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) @@ -1261,6 +1502,7 @@ def test_priority_update(client, data): results = helper_test_http_method(client, 'put', blocked_url, priority_data, users) assert results == [401, 403, 403, 403, 451] + def test_priority_delete(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) @@ -1382,6 +1624,10 @@ def test_priority_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Severity +##################################################### + def test_severity_retrieve(client, data): public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) @@ -1566,6 +1812,10 @@ def test_severity_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Memberships +##################################################### + def test_membership_retrieve(client, data): public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) @@ -1797,14 +2047,15 @@ def test_membership_action_bulk_create(client, data): bulk_data = { "project_id": data.blocked_project.id, "bulk_memberships": [ - {"role_id": data.private_membership2.role.pk, "email": "test1@test.com"}, - {"role_id": data.private_membership2.role.pk, "email": "test2@test.com"}, + {"role_id": data.blocked_membership.role.pk, "email": "test1@test.com"}, + {"role_id": data.blocked_membership.role.pk, "email": "test2@test.com"}, ] } bulk_data = json.dumps(bulk_data) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 403, 451] + def test_membership_action_resend_invitation(client, data): public_invitation = f.InvitationFactory(project=data.public_project, role__project=data.public_project) private_invitation1 = f.InvitationFactory(project=data.private_project1, role__project=data.private_project1) @@ -1837,6 +2088,10 @@ def test_membership_action_resend_invitation(client, data): assert results == [404, 404, 404, 403, 451] +##################################################### +# Project Templates +##################################################### + def test_project_template_retrieve(client, data): url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) @@ -1911,3 +2166,131 @@ def test_project_template_patch(client, data): results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users) assert results == [401, 403, 200] + + +##################################################### +# Tags +##################################################### + +def test_create_tag(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "tag": "testtest", + "color": "#123123" + }) + + url = reverse('projects-create-tag', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-create-tag', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-create-tag', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-create-tag', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] + + +def test_edit_tag(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "from_tag": "tag1", + "to_tag": "renamedtag1", + "color": "#123123" + }) + + url = reverse('projects-edit-tag', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-edit-tag', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-edit-tag', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-edit-tag', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] + + +def test_delete_tag(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "tag": "tag2", + }) + + url = reverse('projects-delete-tag', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-delete-tag', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-delete-tag', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-delete-tag', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] + + +def test_mix_tags(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "from_tags": ["tag1"], + "to_tag": "tag3" + }) + + url = reverse('projects-mix-tags', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-mix-tags', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-mix-tags', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-mix-tags', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 1ff1ca7e..3d62dc6e 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -1,11 +1,31 @@ # -*- 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 django.apps import apps from taiga.base.utils import json from taiga.projects import choices as project_choices -from taiga.projects.serializers import ProjectDetailSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.projects import models as project_models +from taiga.projects.serializers import ProjectSerializer +from taiga.permissions.choices import MEMBERS_PERMISSIONS +from taiga.projects.utils import attach_extra_info from tests import factories as f from tests.utils import helper_test_http_method, helper_test_http_method_and_count @@ -27,19 +47,26 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=['view_project'], public_permissions=['view_project']) + m.public_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=['view_project'], public_permissions=['view_project'], owner=m.project_owner) + m.private_project1 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.private_project2 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.blocked_project.id) f.RoleFactory(project=m.public_project) @@ -135,12 +162,12 @@ def test_project_update(client, data): data.project_owner ] - project_data = ProjectDetailSerializer(data.private_project2).data + project_data = ProjectSerializer(data.private_project2).data project_data["is_private"] = False results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users) assert results == [401, 403, 403, 200] - project_data = ProjectDetailSerializer(data.blocked_project).data + project_data = ProjectSerializer(data.blocked_project).data project_data["is_private"] = False results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users) assert results == [401, 403, 403, 451] @@ -459,6 +486,31 @@ def test_invitations_retrieve(client, data): assert results == [200, 200, 200, 200] +def test_regenerate_epics_csv_uuid(client, data): + public_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 403, 403, 200] + + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 403, 403, 200] + + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 403, 200] + + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 403, 451] + + def test_regenerate_userstories_csv_uuid(client, data): public_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project1.pk}) diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index 6e62df12..7e98b692 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -1,7 +1,25 @@ # -*- 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.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -30,12 +48,12 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, slug="public") m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, slug="private1") m.private_project2 = f.ProjectFactory(is_private=True, @@ -82,6 +100,7 @@ def data(): role__project=m.private_project2, role__permissions=["view_project"]) + m.epic = f.EpicFactory(project=m.private_project2, ref=4) m.us = f.UserStoryFactory(project=m.private_project2, ref=1) m.task = f.TaskFactory(project=m.private_project2, ref=2) m.issue = f.IssueFactory(project=m.private_project2, ref=3) @@ -109,8 +128,9 @@ def test_resolver_list(client, data): assert results == [401, 403, 403, 200, 200] client.login(data.other_user) - response = client.json.get("{}?project={}&us={}&task={}&issue={}&milestone={}".format(url, + response = client.json.get("{}?project={}&epic={}&us={}&task={}&issue={}&milestone={}".format(url, data.private_project2.slug, + data.epic.ref, data.us.ref, data.task.ref, data.issue.ref, @@ -118,18 +138,26 @@ def test_resolver_list(client, data): assert response.data == {"project": data.private_project2.pk} client.login(data.project_owner) - response = client.json.get("{}?project={}&us={}&task={}&issue={}&milestone={}".format(url, + response = client.json.get("{}?project={}&epic={}&us={}&task={}&issue={}&milestone={}".format(url, data.private_project2.slug, + data.epic.ref, data.us.ref, data.task.ref, data.issue.ref, data.milestone.slug)) assert response.data == {"project": data.private_project2.pk, + "epic": data.epic.pk, "us": data.us.pk, "task": data.task.pk, "issue": data.issue.pk, "milestone": data.milestone.pk} + response = client.json.get("{}?project={}&ref={}".format(url, + data.private_project2.slug, + data.epic.ref)) + assert response.data == {"project": data.private_project2.pk, + "epic": data.epic.pk} + response = client.json.get("{}?project={}&ref={}".format(url, data.private_project2.slug, data.us.ref)) diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index 4e08bf1f..633b3131 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -1,7 +1,25 @@ # -*- 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.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method_and_keys, disconnect_signals, reconnect_signals @@ -30,11 +48,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -108,7 +126,7 @@ def test_search_list(client, data): ] results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.public_project.pk}, users) - all_keys = set(['count', 'userstories', 'issues', 'tasks', 'wikipages']) + all_keys = set(['count', 'userstories', 'issues', 'tasks', 'wikipages', 'epics']) assert results == [(200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys)] results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.private_project1.pk}, users) assert results == [(200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys)] diff --git a/tests/integration/resources_permissions/test_storage_resources.py b/tests/integration/resources_permissions/test_storage_resources.py index ebc1eb8a..8d2ddf3b 100644 --- a/tests/integration/resources_permissions/test_storage_resources.py +++ b/tests/integration/resources_permissions/test_storage_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_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index 8e6cc2d7..1efe9a8d 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -22,8 +22,8 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, - ANON_PERMISSIONS, USER_PERMISSIONS) +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS) from tests import factories as f from tests.utils import helper_test_http_method @@ -44,11 +44,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 0d970efb..d2fcc2e3 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -1,16 +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 . + import uuid from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project from taiga.projects.tasks.serializers import TaskSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects.tasks.models import Task +from taiga.projects.tasks.utils import attach_extra_info as attach_task_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f -from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from tests.utils import helper_test_http_method, reconnect_signals from taiga.projects.votes.services import add_vote from taiga.projects.notifications.services import add_watcher @@ -20,10 +42,6 @@ import pytest pytestmark = pytest.mark.django_db -def setup_function(function): - disconnect_signals() - - def setup_function(function): reconnect_signals() @@ -40,50 +58,64 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project1, - user=m.project_member_without_perms, - role__project=m.private_project1, - role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project2, - user=m.project_member_without_perms, - role__project=m.private_project2, - role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -102,8 +134,8 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) milestone_public_task = f.MilestoneFactory(project=m.public_project) milestone_private_task1 = f.MilestoneFactory(project=m.private_project1) @@ -115,21 +147,28 @@ def data(): milestone=milestone_public_task, user_story__project=m.public_project, user_story__milestone=milestone_public_task) + m.public_task = attach_task_extra_info(Task.objects.all()).get(id=m.public_task.id) + m.private_task1 = f.TaskFactory(project=m.private_project1, status__project=m.private_project1, milestone=milestone_private_task1, user_story__project=m.private_project1, user_story__milestone=milestone_private_task1) + m.private_task1 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task1.id) + m.private_task2 = f.TaskFactory(project=m.private_project2, status__project=m.private_project2, milestone=milestone_private_task2, user_story__project=m.private_project2, user_story__milestone=milestone_private_task2) + m.private_task2 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task2.id) + m.blocked_task = f.TaskFactory(project=m.blocked_project, - status__project=m.blocked_project, - milestone=milestone_blocked_task, - user_story__project=m.blocked_project, - user_story__milestone=milestone_blocked_task) + status__project=m.blocked_project, + milestone=milestone_blocked_task, + user_story__project=m.blocked_project, + user_story__milestone=milestone_blocked_task) + m.blocked_task = attach_task_extra_info(Task.objects.all()).get(id=m.blocked_task.id) m.public_project.default_task_status = m.public_task.status m.public_project.save() @@ -143,6 +182,36 @@ def data(): return m +def test_task_list(client, data): + url = reverse('tasks-list') + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 4 + assert response.status_code == 200 + + def test_task_retrieve(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) @@ -167,7 +236,55 @@ def test_task_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_task_update(client, data): +def test_task_create(client, data): + url = reverse('tasks-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "status": data.public_project.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "status": data.private_project1.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "status": data.private_project2.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "status": data.blocked_project.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_put_update(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) @@ -182,32 +299,116 @@ def test_task_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - task_data = TaskSerializer(data.public_task).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', public_url, task_data, users) - assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.public_task).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] - task_data = TaskSerializer(data.private_task1).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', private_url1, task_data, users) - assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.private_task1).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] - task_data = TaskSerializer(data.private_task2).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', private_url2, task_data, users) - assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.private_task2).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] - task_data = TaskSerializer(data.blocked_task).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', blocked_url, task_data, users) - assert results == [401, 403, 403, 451, 451] + task_data = TaskSerializer(data.blocked_task).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] -def test_task_update_with_project_change(client): +def test_task_put_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + task_data = TaskSerializer(data.public_task).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task1).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task2).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.blocked_task).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_put_update_and_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + task_data = TaskSerializer(data.public_task).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task1).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task2).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.blocked_task).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_put_update_with_project_change(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() user3 = f.UserFactory.create() @@ -224,24 +425,28 @@ def test_task_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) task = f.TaskFactory.create(project=project1) + task = attach_task_extra_info(Task.objects.all()).get(id=task.id) url = reverse('tasks-detail', kwargs={"pk": task.pk}) @@ -302,107 +507,7 @@ def test_task_update_with_project_change(client): task.save() -def test_task_delete(client, data): - public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) - private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) - private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) - blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - ] - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url1, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url2, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', blocked_url, None, users) - assert results == [401, 403, 403, 451] - - -def test_task_list(client, data): - url = reverse('tasks-list') - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 4 - assert response.status_code == 200 - - -def test_task_create(client, data): - url = reverse('tasks-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({ - "subject": "test", - "ref": 1, - "project": data.public_project.pk, - "status": data.public_project.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 2, - "project": data.private_project1.pk, - "status": data.private_project1.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.private_project2.pk, - "status": data.private_project2.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.blocked_project.pk, - "status": data.blocked_project.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_task_patch(client, data): +def test_task_patch_update(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) @@ -434,6 +539,108 @@ def test_task_patch(client, data): assert results == [401, 403, 403, 451, 451] +def test_task_patch_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_task.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_task1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_task2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_task.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_patch_update_and_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_task.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_task1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_task2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_task.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_delete(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + def test_task_action_bulk_create(client, data): url = reverse('tasks-bulk-create') @@ -449,7 +656,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.public_task.user_story.pk, "project_id": data.public_task.project.pk, - "sprint_id": data.public_task.milestone.pk, + "milestone_id": data.public_task.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] @@ -458,7 +665,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.private_task1.user_story.pk, "project_id": data.private_task1.project.pk, - "sprint_id": data.private_task1.milestone.pk, + "milestone_id": data.private_task1.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] @@ -467,7 +674,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.private_task2.user_story.pk, "project_id": data.private_task2.project.pk, - "sprint_id": data.private_task2.milestone.pk, + "milestone_id": data.private_task2.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] @@ -476,7 +683,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.blocked_task.user_story.pk, "project_id": data.blocked_task.project.pk, - "sprint_id": data.blocked_task.milestone.pk, + "milestone_id": data.blocked_task.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 451, 451] @@ -557,17 +764,17 @@ def test_task_voters_list(client, data): def test_task_voters_retrieve(client, data): add_vote(data.public_task, data.project_owner) public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.private_task1, data.project_owner) private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.private_task2, data.project_owner) private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.blocked_task, data.project_owner) blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, @@ -587,34 +794,6 @@ def test_task_voters_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_tasks_csv(client, data): - url = reverse('tasks-csv') - csv_public_uuid = data.public_project.tasks_csv_uuid - csv_private1_uuid = data.private_project1.tasks_csv_uuid - csv_private2_uuid = data.private_project1.tasks_csv_uuid - csv_blocked_uuid = data.blocked_project.tasks_csv_uuid - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - def test_task_action_watch(client, data): public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk}) @@ -690,17 +869,17 @@ def test_task_watchers_list(client, data): def test_task_watchers_retrieve(client, data): add_watcher(data.public_task, data.project_owner) public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_task1, data.project_owner) private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_task2, data.project_owner) private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_task, data.project_owner) blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -717,3 +896,31 @@ def test_task_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_tasks_csv(client, data): + url = reverse('tasks-csv') + csv_public_uuid = data.public_project.tasks_csv_uuid + csv_private1_uuid = data.private_project1.tasks_csv_uuid + csv_private2_uuid = data.private_project1.tasks_csv_uuid + csv_blocked_uuid = data.blocked_project.tasks_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py index c921e2e0..a6930a29 100644 --- a/tests/integration/resources_permissions/test_timelines_resources.py +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -1,7 +1,25 @@ # -*- 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.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -30,11 +48,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py index 394dd1f8..b15d9cde 100644 --- a/tests/integration/resources_permissions/test_users_resources.py +++ b/tests/integration/resources_permissions/test_users_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 tempfile import NamedTemporaryFile from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index e23649c3..077219ce 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -22,8 +22,8 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, - ANON_PERMISSIONS, USER_PERMISSIONS) +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS) from tests import factories as f @@ -45,11 +45,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 43f5217d..bf8c596d 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -1,12 +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 +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + import uuid from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.projects.userstories.models import UserStory from taiga.projects.userstories.serializers import UserStorySerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects.userstories.utils import attach_extra_info as attach_userstory_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f @@ -40,50 +62,61 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) - m.blocked_project = f.ProjectFactory(is_private=True, - anon_permissions=[], - public_permissions=[], - owner=m.project_owner, - userstories_csv_uuid=uuid.uuid4().hex, - blocked_code=project_choices.BLOCKED_BY_STAFF) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, role__project=m.private_project1, role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project2, user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -102,8 +135,8 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) @@ -126,19 +159,53 @@ def data(): user_story__milestone__project=m.private_project2, user_story__status__project=m.private_project2) m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0], - points=m.blocked_points, - user_story__project=m.blocked_project, - user_story__milestone__project=m.blocked_project, - user_story__status__project=m.blocked_project) + points=m.blocked_points, + user_story__project=m.blocked_project, + user_story__milestone__project=m.blocked_project, + user_story__status__project=m.blocked_project) m.public_user_story = m.public_role_points.user_story + m.public_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.public_user_story.id) m.private_user_story1 = m.private_role_points1.user_story + m.private_user_story1 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story1.id) m.private_user_story2 = m.private_role_points2.user_story + m.private_user_story2 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story2.id) m.blocked_user_story = m.blocked_role_points.user_story + m.blocked_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.blocked_user_story.id) return m +def test_user_story_list(client, data): + url = reverse('userstories-list') + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 4 + assert response.status_code == 200 + + def test_user_story_retrieve(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) @@ -163,7 +230,35 @@ def test_user_story_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_user_story_update(client, data): +def test_user_story_create(client, data): + url = reverse('userstories-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_put_update(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) @@ -178,31 +273,116 @@ def test_user_story_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - user_story_data = UserStorySerializer(data.public_user_story).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', public_url, user_story_data, users) - assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] - user_story_data = UserStorySerializer(data.private_user_story1).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) - assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] - user_story_data = UserStorySerializer(data.private_user_story2).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) - assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] - user_story_data = UserStorySerializer(data.blocked_user_story).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) - assert results == [401, 403, 403, 451, 451] + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] -def test_user_story_update_with_project_change(client): + +def test_user_story_put_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_put_update_and_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_put_update_with_project_change(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() user3 = f.UserFactory.create() @@ -219,24 +399,28 @@ def test_user_story_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) us = f.UserStoryFactory.create(project=project1) + us = attach_userstory_extra_info(UserStory.objects.all()).get(id=us.id) url = reverse('userstories-detail', kwargs={"pk": us.pk}) @@ -297,6 +481,118 @@ def test_user_story_update_with_project_change(client): us.save() +def test_user_story_patch_update(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_patch_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_user_story.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_user_story1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_user_story2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_user_story.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_patch_update_and_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_user_story.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_user_story1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_user_story2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_user_story.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + def test_user_story_delete(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) @@ -319,96 +615,6 @@ def test_user_story_delete(client, data): assert results == [401, 403, 403, 451] -def test_user_story_list(client, data): - url = reverse('userstories-list') - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 4 - assert response.status_code == 200 - - -def test_user_story_create(client, data): - url = reverse('userstories-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_user_story_patch(client, data): - public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) - private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) - private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) - blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] - - def test_user_story_action_bulk_create(client, data): url = reverse('userstories-bulk-create') @@ -422,11 +628,11 @@ def test_user_story_action_bulk_create(client, data): bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.public_user_story.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.private_user_story1.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.private_user_story2.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) @@ -453,21 +659,21 @@ def test_user_story_action_bulk_update_order(client, data): "project_id": data.public_project.pk }) results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 204, 204] + assert results == [401, 403, 403, 200, 200] post_data = json.dumps({ "bulk_stories": [{"us_id": data.private_user_story1.id, "order": 2}], "project_id": data.private_project1.pk }) results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 204, 204] + assert results == [401, 403, 403, 200, 200] post_data = json.dumps({ "bulk_stories": [{"us_id": data.private_user_story2.id, "order": 2}], "project_id": data.private_project2.pk }) results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 204, 204] + assert results == [401, 403, 403, 200, 200] post_data = json.dumps({ "bulk_stories": [{"us_id": data.blocked_user_story.id, "order": 2}], @@ -562,7 +768,7 @@ def test_user_story_voters_retrieve(client, data): add_vote(data.blocked_user_story, data.project_owner) blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -581,30 +787,6 @@ def test_user_story_voters_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_user_stories_csv(client, data): - url = reverse('userstories-csv') - csv_public_uuid = data.public_project.userstories_csv_uuid - csv_private1_uuid = data.private_project1.userstories_csv_uuid - csv_private2_uuid = data.private_project1.userstories_csv_uuid - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - def test_user_story_action_watch(client, data): public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk}) @@ -680,16 +862,16 @@ def test_userstory_watchers_list(client, data): def test_userstory_watchers_retrieve(client, data): add_watcher(data.public_user_story, data.project_owner) public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_user_story1, data.project_owner) private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_user_story2, data.project_owner) private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_user_story, data.project_owner) blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, @@ -707,3 +889,27 @@ def test_userstory_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_user_stories_action_csv(client, data): + url = reverse('userstories-csv') + csv_public_uuid = data.public_project.userstories_csv_uuid + csv_private1_uuid = data.private_project1.userstories_csv_uuid + csv_private2_uuid = data.private_project1.userstories_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py index dd10f04c..34d4cf00 100644 --- a/tests/integration/resources_permissions/test_webhooks_resources.py +++ b/tests/integration/resources_permissions/test_webhooks_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 @@ -224,16 +242,19 @@ def test_webhook_action_test(client, data): ] with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', url1, None, users) assert results == [404, 404, 200] assert _send_request_mock.called is True with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', url2, None, users) assert results == [404, 404, 404] assert _send_request_mock.called is False with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', blocked_url, None, users) assert results == [404, 404, 451] assert _send_request_mock.called is False diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index 5ccddedb..f4cdbc12 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -1,8 +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 +# 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 -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.projects.notifications.services import add_watcher from taiga.projects.occ import OCCResourceMixin @@ -38,11 +56,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -112,91 +130,9 @@ def data(): return m -def test_wiki_page_retrieve(client, data): - public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) - private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) - private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) - blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url1, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url2, None, users) - assert results == [401, 403, 403, 200, 200] - results = helper_test_http_method(client, 'get', blocked_url, None, users) - assert results == [401, 403, 403, 200, 200] - - -def test_wiki_page_update(client, data): - public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) - private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) - private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) - blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - wiki_page_data = WikiPageSerializer(data.public_wiki_page).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) - assert results == [401, 200, 200, 200, 200] - - wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) - assert results == [401, 200, 200, 200, 200] - - wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) - assert results == [401, 403, 403, 200, 200] - - wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_wiki_page_delete(client, data): - public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) - private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) - private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) - blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - ] - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url1, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url2, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', blocked_url, None, users) - assert results == [401, 403, 403, 451] - +############################################## +## WIKI PAGES +############################################## def test_wiki_page_list(client, data): url = reverse('wiki-list') @@ -228,50 +164,7 @@ def test_wiki_page_list(client, data): assert response.status_code == 200 -def test_wiki_page_create(client, data): - url = reverse('wiki-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({ - "content": "test", - "slug": "test", - "project": data.public_project.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({ - "content": "test", - "slug": "test", - "project": data.private_project1.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({ - "content": "test", - "slug": "test", - "project": data.private_project2.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "content": "test", - "slug": "test", - "project": data.blocked_project.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) - assert results == [401, 403, 403, 451, 451] - -def test_wiki_page_patch(client, data): +def test_wiki_page_retrieve(client, data): public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) @@ -285,54 +178,6 @@ def test_wiki_page_patch(client, data): data.project_owner ] - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 200, 200, 200, 200] - - patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 200, 200, 200, 200] - - patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_wiki_page_action_render(client, data): - url = reverse('wiki-render') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - post_data = json.dumps({"content": "test", "project_id": data.public_project.pk}) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [200, 200, 200, 200, 200] - - -def test_wiki_link_retrieve(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - results = helper_test_http_method(client, 'get', public_url, None, users) assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url1, None, users) @@ -343,11 +188,55 @@ def test_wiki_link_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_wiki_link_update(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) +def test_wiki_page_create(client, data): + url = reverse('wiki-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.public_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.private_project1.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.private_project2.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_put_update(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -358,35 +247,318 @@ def test_wiki_link_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users) - assert results == [401, 200, 200, 200, 200] + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users) - assert results == [401, 200, 200, 200, 200] + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users) - assert results == [401, 403, 403, 451, 451] + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] -def test_wiki_link_delete(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + +def test_wiki_page_put_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_put_update_and_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_put_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + membership1 = f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership2 = f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership3 = f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership4 = f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + wiki_page = f.WikiPageFactory.create(project=project1) + + url = reverse('wiki-detail', kwargs={"pk": wiki_page.pk}) + + # Test user with permissions in both projects + client.login(user1) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 200 + + wiki_page.project = project1 + wiki_page.save() + + # Test user with permissions in only origin project + client.login(user2) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 403 + + wiki_page.project = project1 + wiki_page.save() + + # Test user with permissions in only destionation project + client.login(user3) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 403 + + wiki_page.project = project1 + wiki_page.save() + + # Test user without permissions in the projects + client.login(user4) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 403 + + wiki_page.project = project1 + wiki_page.save() + + +def test_wiki_page_patch_update(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_patch_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_wiki_page.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_wiki_page.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_patch_update_and_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.public_wiki_page.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.private_wiki_page2.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.private_wiki_page2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.blocked_wiki_page.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_delete(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -404,38 +576,8 @@ def test_wiki_link_delete(client, data): assert results == [401, 403, 403, 451] -def test_wiki_link_list(client, data): - url = reverse('wiki-links-list') - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 4 - assert response.status_code == 200 - - -def test_wiki_link_create(client, data): - url = reverse('wiki-links-list') +def test_wiki_page_action_render(client, data): + url = reverse('wiki-render') users = [ None, @@ -445,69 +587,9 @@ def test_wiki_link_create(client, data): data.project_owner ] - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.public_project.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.private_project1.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] - - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.private_project2.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.blocked_project.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 403, 403, 451, 451] - - -def test_wiki_link_patch(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 200, 200, 200, 200] - - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 200, 200, 200, 200] - - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] + post_data = json.dumps({"content": "test", "project_id": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [200, 200, 200, 200, 200] def test_wikipage_action_watch(client, data): @@ -611,3 +693,199 @@ def test_wikipage_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +############################################## +## WIKI LINKS +############################################## + +def test_wiki_link_list(client, data): + url = reverse('wiki-links-list') + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 4 + assert response.status_code == 200 + + +def test_wiki_link_retrieve(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_link_create(client, data): + url = reverse('wiki-links-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.public_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.private_project1.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.private_project2.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_link_update(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_link_patch(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_link_delete(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] diff --git a/tests/integration/test_application_tokens.py b/tests/integration/test_application_tokens.py index b34076b2..4ec5c0cd 100644 --- a/tests/integration/test_application_tokens.py +++ b/tests/integration/test_application_tokens.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.external_apps import encryption diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py index 94e8e3cf..06a49441 100644 --- a/tests/integration/test_attachments.py +++ b/tests/integration/test_attachments.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 . + import pytest from django.core.urlresolvers import reverse diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 5781c58a..0504881b 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -48,6 +48,20 @@ def test_respond_400_when_public_registration_is_disabled(client, register_form, assert response.status_code == 400 +def test_respond_400_when_the_email_domain_isnt_in_allowed_domains(client, register_form, settings): + settings.PUBLIC_REGISTER_ENABLED = True + settings.USER_EMAIL_ALLOWED_DOMAINS = ['other-domain.com'] + response = client.post(reverse("auth-register"), register_form) + assert response.status_code == 400 + + +def test_respond_201_when_the_email_domain_is_in_allowed_domains(client, settings, register_form): + settings.PUBLIC_REGISTER_ENABLED = True + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + response = client.post(reverse("auth-register"), register_form) + assert response.status_code == 201 + + def test_respond_201_with_invitation_without_public_registration(client, register_form, settings): settings.PUBLIC_REGISTER_ENABLED = False user = factories.UserFactory() diff --git a/tests/integration/test_custom_attributes_epics.py b/tests/integration/test_custom_attributes_epics.py new file mode 100644 index 00000000..e24f1d8f --- /dev/null +++ b/tests/integration/test_custom_attributes_epics.py @@ -0,0 +1,201 @@ +# -*- 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.transaction import atomic +from django.core.urlresolvers import reverse +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +######################################################### +# Epic Custom Attributes +######################################################### + +def test_epic_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.EpicCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_admin=True) + + + url = reverse("epic-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_epic_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.EpicCustomAttributeFactory() + custom_attr_2 = f.EpicCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_admin=True) + + + url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_epic_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.EpicCustomAttributeFactory() + custom_attr_2 = f.EpicCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_admin=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_admin=True) + + url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# Epic Custom Attributes Values +######################################################### + +def test_epic_custom_attributes_values_when_create_us(client): + epic = f.EpicFactory() + assert epic.custom_attributes_values.attributes_values == {} + + +def test_epic_custom_attributes_values_update(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = epic.custom_attributes_values + + url = reverse("epic-custom-attributes-values-detail", args=[epic.id]) + data = { + "attributes_values": { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + }, + "version": custom_attrs_val.version + } + + + assert epic.custom_attributes_values.attributes_values == {} + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data["attributes_values"] == data["attributes_values"] + epic = epic.__class__.objects.get(id=epic.id) + assert epic.custom_attributes_values.attributes_values == data["attributes_values"] + + +def test_epic_custom_attributes_values_update_with_error_invalid_key(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + + custom_attrs_val = epic.custom_attributes_values + + url = reverse("epic-custom-attributes-values-detail", args=[epic.id]) + data = { + "attributes_values": { + ct1_id: "test_1_updated", + "123456": "test_2_updated" + }, + "version": custom_attrs_val.version + } + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + +def test_epic_custom_attributes_values_delete_epic(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = epic.custom_attributes_values + + url = reverse("epics-detail", args=[epic.id]) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert not epic.__class__.objects.filter(id=epic.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +######################################################### +# Test tristres triggers :-P +######################################################### + +def test_trigger_update_epiccustomvalues_afeter_remove_epiccustomattribute(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = epic.custom_attributes_values + custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"} + custom_attrs_val.save() + + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id in custom_attrs_val.attributes_values.keys() + + url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id not in custom_attrs_val.attributes_values.keys() diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py new file mode 100644 index 00000000..a95f1c7c --- /dev/null +++ b/tests/integration/test_epics.py @@ -0,0 +1,164 @@ +# -*- 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 . + +import uuid +import csv + +from unittest import mock + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.epics import services +from taiga.projects.epics import models + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_get_invalid_csv(client): + url = reverse("epics-csv") + + response = client.get(url) + assert response.status_code == 404 + + response = client.get("{}?uuid={}".format(url, "not-valid-uuid")) + assert response.status_code == 404 + + +def test_get_valid_csv(client): + url = reverse("epics-csv") + project = f.ProjectFactory.create(epics_csv_uuid=uuid.uuid4().hex) + + response = client.get("{}?uuid={}".format(url, project.epics_csv_uuid)) + assert response.status_code == 200 + + +def test_custom_fields_csv_generation(): + project = f.ProjectFactory.create(epics_csv_uuid=uuid.uuid4().hex) + attr = f.EpicCustomAttributeFactory.create(project=project, name="attr1", description="desc") + epic = f.EpicFactory.create(project=project) + attr_values = epic.custom_attributes_values + attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.save() + queryset = project.epics.all() + data = services.epics_to_csv(project, queryset) + data.seek(0) + reader = csv.reader(data) + row = next(reader) + assert row[18] == attr.name + row = next(reader) + assert row[18] == "val1" + + +def test_update_epic_order(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + epic_1 = f.EpicFactory.create(project=project, epics_order=1, status=project.default_us_status) + epic_2 = f.EpicFactory.create(project=project, epics_order=2, status=project.default_us_status) + epic_3 = f.EpicFactory.create(project=project, epics_order=3, status=project.default_us_status) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + url = reverse('epics-detail', kwargs={"pk": epic_1.pk}) + data = { + "epics_order": 2, + "version": epic_1.version + } + + client.login(user) + response = client.json.patch(url, json.dumps(data)) + assert json.loads(response.get("taiga-info-order-updated")) == { + str(epic_1.id): 2, + str(epic_2.id): 3, + str(epic_3.id): 4 + } + + +def test_bulk_create_related_userstories(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + epic = f.EpicFactory.create(project=project) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + url = reverse('epics-related-userstories-bulk-create', args=[epic.pk]) + + data = { + "bulk_userstories": "test1\ntest2", + "project_id": project.id + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + assert len(response.data) == 2 + + +def test_set_related_userstory(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) + + url = reverse('epics-related-userstories-list', args=[epic.pk]) + + data = { + "user_story": us.id, + "epic": epic.pk + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + +def test_set_related_userstory_existing(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) + + url = reverse('epics-related-userstories-detail', args=[epic.pk, us.pk]) + data = { + "order": 77 + } + client.login(user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + + related_us.refresh_from_db() + assert related_us.order == 77 + + +def test_unset_related_userstory(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + + url = reverse('epics-related-userstories-detail', args=[epic.pk, us.id]) + + client.login(user) + response = client.delete(url) + assert response.status_code == 204 + assert not models.RelatedUserStory.objects.filter(id=related_us.id).exists() diff --git a/tests/integration/test_epics_tags.py b/tests/integration/test_epics_tags.py new file mode 100644 index 00000000..3e2c66c8 --- /dev/null +++ b/tests/integration/test_epics_tags.py @@ -0,0 +1,161 @@ +# -*- 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 unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_epic_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + epic = f.create_epic(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=epic.owner, is_admin=True) + url = reverse("epics-detail", kwargs={"pk": epic.pk}) + data = { + "tags": [], + "version": epic.version + } + + client.login(epic.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_epic_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + epic = f.create_epic(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=epic.owner, is_admin=True) + url = reverse("epics-detail", kwargs={"pk": epic.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": epic.version + } + + client.login(epic.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_epic_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + epic = f.create_epic(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=epic.owner, is_admin=True) + url = reverse("epics-detail", kwargs={"pk": epic.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": epic.version + } + + client.login(epic.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_epic_with_tags(client): + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) + status = f.EpicStatusFactory.create(project=project) + project.default_epic_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("epics-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + epic_tags_colors = OrderedDict(response.data["tags"]) + + assert epic_tags_colors["back"] == "#fff8e7" + assert epic_tags_colors["front"] == "#aaaaaa" + assert epic_tags_colors["ux"] == "#fabada" + + tags_colors = OrderedDict(project.tags_colors) + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_feedback.py b/tests/integration/test_feedback.py index 777c65c8..82de1139 100644 --- a/tests/integration/test_feedback.py +++ b/tests/integration/test_feedback.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 tests import factories as f diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index e73b1fce..cc6d8d43 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -18,6 +18,8 @@ # along with this program. If not, see . import pytest +import datetime + from unittest.mock import patch from django.core.urlresolvers import reverse @@ -226,6 +228,7 @@ def test_delete_comment_by_project_owner(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) key = make_key_from_model_object(us) history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=project, comment="testing", key=key, diff={}, @@ -236,3 +239,87 @@ def test_delete_comment_by_project_owner(client): url = "%s?id=%s" % (url, history_entry.id) response = client.post(url, content_type="application/json") assert 200 == response.status_code, response.status_code + + +def test_edit_comment(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=project, + comment="testing", + key=key, + diff={}, + user={"pk": project.owner.id}) + + history_entry_created_at = history_entry.created_at + assert history_entry.comment_versions == None + assert history_entry.edit_comment_date == None + + client.login(project.owner) + url = reverse("userstory-history-edit-comment", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + data = json.dumps({"comment": "testing update comment"}) + response = client.post(url, data, content_type="application/json") + assert 200 == response.status_code, response.status_code + + + history_entry = HistoryEntry.objects.get(id=history_entry.id) + assert len(history_entry.comment_versions) == 1 + assert history_entry.comment == "testing update comment" + assert history_entry.comment_versions[0]["comment"] == "testing" + assert history_entry.edit_comment_date != None + assert history_entry.comment_versions[0]["user"]["id"] == project.owner.id + + +def test_get_comment_versions(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create( + project=project, + type=HistoryType.change, + comment="testing", + key=key, + diff={}, + user={"pk": project.owner.id}, + edit_comment_date=datetime.datetime.now(), + comment_versions = [{ + "comment_html": "

test

", + "date": "2016-05-09T09:34:27.221Z", + "comment": "test", + "user": { + "id": project.owner.id, + }}]) + + client.login(project.owner) + url = reverse("userstory-history-comment-versions", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + response = client.get(url, content_type="application/json") + assert 200 == response.status_code, response.status_code + assert response.data[0]["user"]["username"] == project.owner.username + + +def test_get_comment_versions_from_history_entry_without_comment(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create( + project=project, + type=HistoryType.change, + key=key, + diff={}, + user={"pk": project.owner.id}) + + client.login(project.owner) + url = reverse("userstory-history-comment-versions", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + response = client.get(url, content_type="application/json") + assert 200 == response.status_code, response.status_code + assert response.data == None diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 4b278f38..6dbe06c3 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.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 . + import pytest import urllib @@ -13,6 +31,7 @@ from taiga.hooks.bitbucket import event_hooks from taiga.hooks.bitbucket.api import BitBucketViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.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 @@ -221,6 +240,38 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s #%s ok bye!" % (epic.ref, new_status.slug) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -228,6 +279,13 @@ def test_push_event_issue_processing(client): new_status = f.IssueStatusFactory(project=creation_status.project) issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -253,6 +311,13 @@ def test_push_event_task_processing(client): new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -278,6 +343,13 @@ def test_push_event_user_story_processing(client): new_status = f.UserStoryStatusFactory(project=creation_status.project) user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -296,6 +368,108 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (issue.ref) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (task.ref) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (user_story.ref) } + ] + } + ] + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -304,6 +478,13 @@ def test_push_event_multiple_actions(client): issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -331,6 +512,13 @@ def test_push_event_processing_case_insensitive(client): new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -352,6 +540,43 @@ def test_push_event_processing_case_insensitive(client): def test_push_event_task_bad_processing_non_existing_ref(client): issue_status = f.IssueStatusFactory() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-6666666 #%s ok bye!" % (issue_status.slug) } + ] + } + ] + } + } + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue_status.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The referenced element doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_task_bad_processing_non_existing_ref(client): + issue_status = f.IssueStatusFactory() + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -375,6 +600,13 @@ def test_push_event_task_bad_processing_non_existing_ref(client): def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -399,6 +631,13 @@ def test_push_event_us_bad_processing_non_existing_status(client): def test_push_event_bad_processing_non_existing_status(client): issue = f.IssueFactory.create() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -668,8 +907,10 @@ def test_api_patch_project_modules(client): def test_replace_bitbucket_references(): - assert event_hooks.replace_bitbucket_references("project-url", "#2") == "[BitBucket#2](project-url/issues/2)" - assert event_hooks.replace_bitbucket_references("project-url", "#2 ") == "[BitBucket#2](project-url/issues/2) " - assert event_hooks.replace_bitbucket_references("project-url", " #2 ") == " [BitBucket#2](project-url/issues/2) " - assert event_hooks.replace_bitbucket_references("project-url", " #2") == " [BitBucket#2](project-url/issues/2)" - assert event_hooks.replace_bitbucket_references("project-url", "#test") == "#test" + ev_hook = event_hooks.BaseBitBucketEventHook + assert ev_hook.replace_bitbucket_references(None, "project-url", "#2") == "[BitBucket#2](project-url/issues/2)" + assert ev_hook.replace_bitbucket_references(None, "project-url", "#2 ") == "[BitBucket#2](project-url/issues/2) " + assert ev_hook.replace_bitbucket_references(None, "project-url", " #2 ") == " [BitBucket#2](project-url/issues/2) " + assert ev_hook.replace_bitbucket_references(None, "project-url", " #2") == " [BitBucket#2](project-url/issues/2)" + assert ev_hook.replace_bitbucket_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_bitbucket_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index f472f479..1bcbcdbe 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.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 . + import pytest from unittest import mock @@ -11,6 +29,7 @@ from taiga.hooks.github import event_hooks from taiga.hooks.github.api import GitHubViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.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 @@ -93,6 +112,26 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s #%s ok + bye! + """ % (epic.ref, new_status.slug)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -154,6 +193,70 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (issue.ref)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (task.ref)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (user_story.ref)}, + ]} + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -436,7 +539,7 @@ def test_issues_event_bad_comment(client): take_snapshot(issue, user=issue.owner) payload = { - "action": "other", + "action": "created", "issue": {}, "comment": {}, "repository": { @@ -494,9 +597,10 @@ def test_api_patch_project_modules(client): def test_replace_github_references(): - assert event_hooks.replace_github_references("project-url", "#2") == "[GitHub#2](project-url/issues/2)" - assert event_hooks.replace_github_references("project-url", "#2 ") == "[GitHub#2](project-url/issues/2) " - assert event_hooks.replace_github_references("project-url", " #2 ") == " [GitHub#2](project-url/issues/2) " - assert event_hooks.replace_github_references("project-url", " #2") == " [GitHub#2](project-url/issues/2)" - assert event_hooks.replace_github_references("project-url", "#test") == "#test" - assert event_hooks.replace_github_references("project-url", None) == "" + ev_hook = event_hooks.BaseGitHubEventHook + assert ev_hook.replace_github_references(None, "project-url", "#2") == "[GitHub#2](project-url/issues/2)" + assert ev_hook.replace_github_references(None, "project-url", "#2 ") == "[GitHub#2](project-url/issues/2) " + assert ev_hook.replace_github_references(None, "project-url", " #2 ") == " [GitHub#2](project-url/issues/2) " + assert ev_hook.replace_github_references(None, "project-url", " #2") == " [GitHub#2](project-url/issues/2)" + assert ev_hook.replace_github_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_github_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index bf86a80c..b40c94e6 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -1,5 +1,24 @@ # -*- 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 . + import pytest +from copy import deepcopy from unittest import mock @@ -11,6 +30,7 @@ from taiga.hooks.gitlab import event_hooks from taiga.hooks.gitlab.api import GitLabViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.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 @@ -23,6 +43,189 @@ from .. import factories as f pytestmark = pytest.mark.django_db +push_base_payload = { + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project": { + "name": "Diaspora", + "description": "", + "web_url": "http://example.com/mike/diaspora", + "avatar_url": None, + "git_ssh_url": "git@example.com:mike/diaspora.git", + "git_http_url": "http://example.com/mike/diaspora.git", + "namespace": "Mike", + "visibility_level": 0, + "path_with_namespace": "mike/diaspora", + "default_branch": "master", + "homepage": "http://example.com/mike/diaspora", + "url": "git@example.com:mike/diaspora.git", + "ssh_url": "git@example.com:mike/diaspora.git", + "http_url": "http://example.com/mike/diaspora.git" + }, + "repository": { + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url": "http://example.com/mike/diaspora.git", + "git_ssh_url": "git@example.com:mike/diaspora.git", + "visibility_level": 0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +} + +new_issue_base_payload = { + "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project": { + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlabhq/gitlab-test", + "avatar_url": None, + "git_ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "git_http_url": "http://example.com/gitlabhq/gitlab-test.git", + "namespace": "GitlabHQ", + "visibility_level": 20, + "path_with_namespace": "gitlabhq/gitlab-test", + "default_branch": "master", + "homepage": "http://example.com/gitlabhq/gitlab-test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "http_url": "http://example.com/gitlabhq/gitlab-test.git" + }, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 301, + "title": "New API: create/update/delete file", + "assignee_id": 51, + "author_id": 51, + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "position": 0, + "branch_name": None, + "description": "Create new API for manipulations with repository", + "milestone_id": None, + "state": "opened", + "iid": 23, + "url": "http://example.com/diaspora/issues/23", + "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } +} + +issue_comment_base_payload = { + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project": { + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlab-org/gitlab-test", + "avatar_url": None, + "git_ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "git_http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10, + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master", + "homepage": "http://example.com/gitlab-org/gitlab-test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git" + }, + "repository": { + "name": "diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": None, + "line_code": None, + "commit_id": "", + "noteable_id": 92, + "system": False, + "st_diff": None, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": None, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": None, + "description": "test", + "milestone_id": None, + "state": "closed", + "iid": 17 + } +} def test_bad_signature(client): project = f.ProjectFactory() @@ -72,8 +275,8 @@ def test_ok_empty_payload(client): url = reverse("gitlab-hook-list") url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") - data = {} - response = client.post(url,"null", content_type="application/json", REMOTE_ADDR="111.111.111.111") + response = client.post(url, "null", content_type="application/json", + REMOTE_ADDR="111.111.111.111") assert response.status_code == 204 @@ -90,8 +293,7 @@ def test_ok_signature_ip_in_network(client): url = reverse("gitlab-hook-list") url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") data = {"test:": "data"} - response = client.post(url, - json.dumps(data), + response = client.post(url, json.dumps(data), content_type="application/json", REMOTE_ADDR="111.111.111.112") @@ -225,9 +427,13 @@ def test_push_event_detected(client): project = f.ProjectFactory() url = reverse("gitlab-hook-list") url = "%s?project=%s" % (url, project.id) - data = {"commits": [ - {"message": "test message"}, - ]} + data = deepcopy(push_base_payload) + data["commits"] = [{ + "message": "test message", + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + data["total_commits_count"] = 1 GitLabViewSet._validate_signature = mock.Mock(return_value=True) @@ -241,18 +447,46 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s #%s ok + bye! + """ % (epic.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.IssueStatusFactory(project=creation_status.project) issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message - test TG-%s #%s ok - bye! - """ % (issue.ref, new_status.slug)}, - ]} + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s #%s ok + bye! + """ % (issue.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(issue.project, payload) ev_hook.process_event() @@ -267,12 +501,16 @@ def test_push_event_task_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok bye! - """ % (task.ref, new_status.slug)}, - ]} + """ % (task.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(task.project, payload) ev_hook.process_event() @@ -287,12 +525,16 @@ def test_push_event_user_story_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.UserStoryStatusFactory(project=creation_status.project) user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok bye! - """ % (user_story.ref, new_status.slug)}, - ]} + """ % (user_story.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(user_story.project, payload) @@ -302,6 +544,79 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (issue.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (task.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (user_story.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -309,13 +624,17 @@ def test_push_event_multiple_actions(client): new_status = f.IssueStatusFactory(project=creation_status.project) issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok test TG-%s #%s ok bye! - """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug)}, - ]} + """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook1 = event_hooks.PushEventHook(issue1.project, payload) ev_hook1.process_event() @@ -332,12 +651,16 @@ def test_push_event_processing_case_insensitive(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test tg-%s #%s ok bye! - """ % (task.ref, new_status.slug.upper())}, - ]} + """ % (task.ref, new_status.slug.upper()), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(task.project, payload) ev_hook.process_event() @@ -348,12 +671,16 @@ def test_push_event_processing_case_insensitive(client): def test_push_event_task_bad_processing_non_existing_ref(client): issue_status = f.IssueStatusFactory() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-6666666 #%s ok bye! - """ % (issue_status.slug)}, - ]} + """ % (issue_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(issue_status.project, payload) @@ -366,12 +693,16 @@ def test_push_event_task_bad_processing_non_existing_ref(client): def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #non-existing-slug ok bye! - """ % (user_story.ref)}, - ]} + """ % (user_story.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] @@ -385,12 +716,16 @@ def test_push_event_us_bad_processing_non_existing_status(client): def test_push_event_bad_processing_non_existing_status(client): issue = f.IssueFactory.create() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #non-existing-slug ok bye! - """ % (issue.ref)}, - ]} + """ % (issue.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] @@ -414,15 +749,12 @@ def test_issues_event_opened_issue(client): notify_policy.notify_level = NotifyLevel.all notify_policy.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "title": "test-title", - "description": "test-body", - "url": "http://gitlab.com/test/project/issues/11", - "action": "open", - }, - } + payload = deepcopy(new_issue_base_payload) + payload["object_attributes"]["title"] = "test-title" + payload["object_attributes"]["description"] = "test-body" + payload["object_attributes"]["url"] = "http://gitlab.com/test/project/issues/11" + payload["object_attributes"]["action"] = "open" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -441,15 +773,12 @@ def test_issues_event_other_than_opened_issue(client): issue.project.default_priority = issue.priority issue.project.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "title": "test-title", - "description": "test-body", - "url": "http://gitlab.com/test/project/issues/11", - "action": "update", - }, - } + payload = deepcopy(new_issue_base_payload) + payload["object_attributes"]["title"] = "test-title" + payload["object_attributes"]["description"] = "test-body" + payload["object_attributes"]["url"] = "http://gitlab.com/test/project/issues/11" + payload["object_attributes"]["action"] = "update" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -468,12 +797,12 @@ def test_issues_event_bad_issue(client): issue.project.default_priority = issue.priority issue.project.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "action": "open", - }, - } + payload = deepcopy(new_issue_base_payload) + del payload["object_attributes"]["title"] + del payload["object_attributes"]["description"] + del payload["object_attributes"]["url"] + payload["object_attributes"]["action"] = "open" + payload["repository"]["homepage"] = "test" mail.outbox = [] ev_hook = event_hooks.IssuesEventHook(issue.project, payload) @@ -500,23 +829,13 @@ def test_issue_comment_event_on_existing_issue_task_and_us(client): us = f.UserStoryFactory.create(external_reference=["gitlab", "http://gitlab.com/test/project/issues/11"], owner=project.owner, project=project) take_snapshot(us, user=user) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "11", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - "note": "Test body", - }, - "repository": { - "homepage": "http://gitlab.com/test/project", - }, - } - + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "11" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + payload["object_attributes"]["note"] = "Test body" + payload["repository"]["homepage"] = "http://gitlab.com/test/project" mail.outbox = [] @@ -550,22 +869,13 @@ def test_issue_comment_event_on_not_existing_issue_task_and_us(client): us = f.UserStoryFactory.create(project=issue.project, external_reference=["gitlab", "10"]) take_snapshot(us, user=us.owner) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "99999", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - "note": "test comment", - }, - "repository": { - "homepage": "test", - }, - } + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "99999" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + payload["object_attributes"]["note"] = "test comment" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -587,21 +897,14 @@ def test_issues_event_bad_comment(client): issue = f.IssueFactory.create(external_reference=["gitlab", "10"]) take_snapshot(issue, user=issue.owner) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "10", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - }, - "repository": { - "homepage": "test", - }, - } + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "10" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + del payload["object_attributes"]["note"] + payload["repository"]["homepage"] = "test" + ev_hook = event_hooks.IssueCommentEventHook(issue.project, payload) mail.outbox = [] @@ -653,9 +956,10 @@ def test_api_patch_project_modules(client): def test_replace_gitlab_references(): - assert event_hooks.replace_gitlab_references("project-url", "#2") == "[GitLab#2](project-url/issues/2)" - assert event_hooks.replace_gitlab_references("project-url", "#2 ") == "[GitLab#2](project-url/issues/2) " - assert event_hooks.replace_gitlab_references("project-url", " #2 ") == " [GitLab#2](project-url/issues/2) " - assert event_hooks.replace_gitlab_references("project-url", " #2") == " [GitLab#2](project-url/issues/2)" - assert event_hooks.replace_gitlab_references("project-url", "#test") == "#test" - assert event_hooks.replace_gitlab_references("project-url", None) == "" + ev_hook = event_hooks.BaseGitLabEventHook + assert ev_hook.replace_gitlab_references(None, "project-url", "#2") == "[GitLab#2](project-url/issues/2)" + assert ev_hook.replace_gitlab_references(None, "project-url", "#2 ") == "[GitLab#2](project-url/issues/2) " + assert ev_hook.replace_gitlab_references(None, "project-url", " #2 ") == " [GitLab#2](project-url/issues/2) " + assert ev_hook.replace_gitlab_references(None, "project-url", " #2") == " [GitLab#2](project-url/issues/2)" + assert ev_hook.replace_gitlab_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_gitlab_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_gogs.py b/tests/integration/test_hooks_gogs.py new file mode 100644 index 00000000..11685d1a --- /dev/null +++ b/tests/integration/test_hooks_gogs.py @@ -0,0 +1,533 @@ +# -*- 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 . + +import pytest + +from unittest import mock + +from django.core.urlresolvers import reverse +from django.core import mail + +from taiga.base.utils import json +from taiga.hooks.gogs import event_hooks +from taiga.hooks.gogs.api import GogsViewSet +from taiga.hooks.exceptions import ActionSyntaxException +from taiga.projects import choices as project_choices +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.models import Membership +from taiga.projects.history.services import get_history_queryset_by_model_instance, take_snapshot +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.models import NotifyPolicy +from taiga.projects import services +from .. import factories as f + +pytestmark = pytest.mark.django_db + + +def test_bad_signature(client): + project = f.ProjectFactory() + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = { + "secret": "badbadbad" + } + response = client.post(url, json.dumps(data), + content_type="application/json") + response_content = response.data + assert response.status_code == 400 + assert "Bad signature" in response_content["_error_message"] + + +def test_ok_signature(client): + project = f.ProjectFactory() + f.ProjectModulesConfigFactory(project=project, config={ + "gogs": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data", "secret": "tpnIwJDz4e"} + response = client.post(url, json.dumps(data), + content_type="application/json") + + assert response.status_code == 204 + + +def test_blocked_project(client): + project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF) + f.ProjectModulesConfigFactory(project=project, config={ + "gogs": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data", "secret": "tpnIwJDz4e"} + response = client.post(url, json.dumps(data), + content_type="application/json") + + assert response.status_code == 451 + + +def test_push_event_detected(client): + project = f.ProjectFactory() + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = { + "commits": [ + { + "message": "test message", + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + + GogsViewSet._validate_signature = mock.Mock(return_value=True) + + with mock.patch.object(event_hooks.PushEventHook, "process_event") as process_event_mock: + response = client.post(url, json.dumps(data), + HTTP_X_GITHUB_EVENT="push", + content_type="application/json") + + assert process_event_mock.call_count == 1 + + assert response.status_code == 204 + + +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (epic.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_issue_processing(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.IssueStatusFactory(project=creation_status.project) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (issue.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue = Issue.objects.get(id=issue.id) + assert issue.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_task_processing(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.TaskStatusFactory(project=creation_status.project) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (task.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task = Task.objects.get(id=task.id) + assert task.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_processing(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.UserStoryStatusFactory(project=creation_status.project) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (user_story.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + user_story = UserStory.objects.get(id=user_story.id) + assert user_story.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (issue.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (task.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (user_story.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_multiple_actions(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.IssueStatusFactory(project=creation_status.project) + issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + test TG-%s #%s ok + bye! + """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook1 = event_hooks.PushEventHook(issue1.project, payload) + ev_hook1.process_event() + issue1 = Issue.objects.get(id=issue1.id) + issue2 = Issue.objects.get(id=issue2.id) + assert issue1.status.id == new_status.id + assert issue2.status.id == new_status.id + assert len(mail.outbox) == 2 + + +def test_push_event_processing_case_insensitive(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.TaskStatusFactory(project=creation_status.project) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test tg-%s #%s ok + bye! + """ % (task.ref, new_status.slug.upper()), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task = Task.objects.get(id=task.id) + assert task.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_task_bad_processing_non_existing_ref(client): + issue_status = f.IssueStatusFactory() + payload = { + "commits": [ + { + "message": """test message + test TG-6666666 #%s ok + bye! + """ % (issue_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue_status.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The referenced element doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_us_bad_processing_non_existing_status(client): + user_story = f.UserStoryFactory.create() + payload = { + "commits": [ + { + "message": """test message + test TG-%s #non-existing-slug ok + bye! + """ % (user_story.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The status doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_bad_processing_non_existing_status(client): + issue = f.IssueFactory.create() + payload = { + "commits": [ + { + "message": """test message + test TG-%s #non-existing-slug ok + bye! + """ % (issue.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "html_url": "http://test-url/test/project" + } + } + + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The status doesn't exist" + assert len(mail.outbox) == 0 + + +def test_api_get_project_modules(client): + project = f.create_project() + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("projects-modules", args=(project.id,)) + + client.login(project.owner) + response = client.get(url) + assert response.status_code == 200 + content = response.data + assert "gogs" in content + assert content["gogs"]["secret"] != "" + assert content["gogs"]["webhooks_url"] != "" + + +def test_api_patch_project_modules(client): + project = f.create_project() + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("projects-modules", args=(project.id,)) + + client.login(project.owner) + data = { + "gogs": { + "secret": "test_secret", + "html_url": "test_url", + } + } + response = client.patch(url, json.dumps(data), content_type="application/json") + assert response.status_code == 204 + + config = services.get_modules_config(project).config + assert "gogs" in config + assert config["gogs"]["secret"] == "test_secret" + assert config["gogs"]["webhooks_url"] != "test_url" + + +def test_replace_gogs_references(): + ev_hook = event_hooks.BaseGogsEventHook + assert ev_hook.replace_gogs_references(None, "project-url", "#2") == "[Gogs#2](project-url/issues/2)" + assert ev_hook.replace_gogs_references(None, "project-url", "#2 ") == "[Gogs#2](project-url/issues/2) " + assert ev_hook.replace_gogs_references(None, "project-url", " #2 ") == " [Gogs#2](project-url/issues/2) " + assert ev_hook.replace_gogs_references(None, "project-url", " #2") == " [Gogs#2](project-url/issues/2)" + assert ev_hook.replace_gogs_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_gogs_references(None, "project-url", None) == "" diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 490ba83b..6a2e7883 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -68,7 +68,7 @@ def test_valid_project_import_without_extra_data(client): } response = client.json.post(url, json.dumps(data)) - assert response.status_code == 201 + assert response.status_code == 201, response.data must_empty_children = [ "issues", "user_stories", "us_statuses", "wiki_pages", "priorities", "severities", "milestones", "points", "issue_types", "task_statuses", diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 16fc156f..b2055bc4 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -1,6 +1,28 @@ # -*- 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 . + import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock @@ -38,15 +60,6 @@ Issue #2 db.save_in_bulk.assert_called_once_with(issues, None, None) -def test_update_issues_order_in_bulk(): - data = [(1, 1), (2, 2)] - - with mock.patch("taiga.projects.issues.services.db") as db: - services.update_issues_order_in_bulk(data) - db.update_in_bulk_with_ids.assert_called_once_with([1, 2], [{"order": 1}, {"order": 2}], - model=models.Issue) - - def test_create_issue_without_status(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -211,6 +224,150 @@ def test_api_filter_by_text_6(client): assert response.status_code == 200 assert number_of_issues == 1 + +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_created_date__gt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__gt=%s" % ( + quote(one_day_ago.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_created_date__gte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__gte=%s" % ( + quote(one_day_ago.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 2 + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__lt=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["ref"] == old_issue.ref + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__lte=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + _day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + older_issue = f.create_issue(owner=user) + issue = f.create_issue(owner=user) + # we have to refresh as it slightly differs + issue.refresh_from_db() + + assert older_issue.modified_date < issue.modified_date + + url = reverse("issues-list") + "?modified_date__gte=%s" % ( + quote(issue.modified_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_finished_date(client): + user = f.UserFactory(is_superuser=True) + project = f.ProjectFactory.create() + status0 = f.IssueStatusFactory.create(project=project, is_closed=True) + + issue = f.create_issue(owner=user) + finished_issue = f.create_issue(owner=user, status=status0) + + assert finished_issue.finished_date + + url = reverse("issues-list") + "?finished_date__gte=%s" % ( + quote(finished_issue.finished_date.isoformat()) + ) + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == finished_issue.ref + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) @@ -360,8 +517,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 @@ -397,8 +553,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 diff --git a/tests/integration/test_issues_tags.py b/tests/integration/test_issues_tags.py new file mode 100644 index 00000000..cb355f8f --- /dev/null +++ b/tests/integration/test_issues_tags.py @@ -0,0 +1,161 @@ +# -*- 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 unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_issue_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + issue = f.create_issue(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True) + url = reverse("issues-detail", kwargs={"pk": issue.pk}) + data = { + "tags": [], + "version": issue.version + } + + client.login(issue.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_issue_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + issue = f.create_issue(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True) + url = reverse("issues-detail", kwargs={"pk": issue.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": issue.version + } + + client.login(issue.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_issue_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + issue = f.create_issue(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True) + url = reverse("issues-detail", kwargs={"pk": issue.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": issue.version + } + + client.login(issue.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_issue_with_tags(client): + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) + status = f.IssueStatusFactory.create(project=project) + project.default_issue_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("issues-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + issue_tags_colors = OrderedDict(response.data["tags"]) + + assert issue_tags_colors["back"] == "#fff8e7" + assert issue_tags_colors["front"] == "#aaaaaa" + assert issue_tags_colors["ux"] == "#fabada" + + tags_colors = OrderedDict(project.tags_colors) + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index 580378f5..70d3d198 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.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 unittest import mock from django.core.urlresolvers import reverse @@ -54,6 +72,103 @@ def test_api_create_bulk_members(client): assert response.data[1]["email"] == joseph.email +def test_api_create_bulk_members_with_invalid_roles(client): + project = f.ProjectFactory() + john = f.UserFactory.create() + joseph = f.UserFactory.create() + tester = f.RoleFactory(name="Tester") + gamer = f.RoleFactory(name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": john.email}, + {"role_id": gamer.pk, "email": joseph.email}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "bulk_memberships" in response.data + + +def test_api_create_bulk_members_with_allowed_domain(client): + project = f.ProjectFactory() + john = f.UserFactory.create() + joseph = f.UserFactory.create() + tester = f.RoleFactory(project=project, name="Tester") + gamer = f.RoleFactory(project=project, name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": "test1@email.com"}, + {"role_id": gamer.pk, "email": "test2@email.com"}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert response.data[0]["email"] == "test1@email.com" + assert response.data[1]["email"] == "test2@email.com" + + +def test_api_create_bulk_members_with_allowed_and_unallowed_domain(client, settings): + project = f.ProjectFactory() + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + tester = f.RoleFactory(project=project, name="Tester") + gamer = f.RoleFactory(project=project, name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": "test@invalid-domain.com"}, + {"role_id": gamer.pk, "email": "test@email.com"}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "email" in response.data["bulk_memberships"][0] + assert "email" not in response.data["bulk_memberships"][1] + + +def test_api_create_bulk_members_with_unallowed_domains(client, settings): + project = f.ProjectFactory() + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + tester = f.RoleFactory(project=project, name="Tester") + gamer = f.RoleFactory(project=project, name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": "test1@invalid-domain.com"}, + {"role_id": gamer.pk, "email": "test2@invalid-domain.com"}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "email" in response.data["bulk_memberships"][0] + assert "email" in response.data["bulk_memberships"][1] + + def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client): user = f.UserFactory.create(max_memberships_private_projects=3) project = f.ProjectFactory(owner=user, is_private=True) @@ -296,6 +411,36 @@ def test_api_create_membership(client): assert response.data["user_email"] == user.email +def test_api_create_membership_with_unallowed_domain(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + + membership = f.MembershipFactory(is_admin=True) + role = f.RoleFactory.create(project=membership.project) + + client.login(membership.user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": role.project.pk, "email": "test@invalid-email.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "email" in response.data + + +def test_api_create_membership_with_allowed_domain(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + + membership = f.MembershipFactory(is_admin=True) + role = f.RoleFactory.create(project=membership.project) + + client.login(membership.user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": role.project.pk, "email": "test@email.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + assert response.data["email"] == "test@email.com" + + def test_api_create_membership_without_enough_memberships_private_project_slots_one_projects(client): user = f.UserFactory.create(max_memberships_private_projects=1) project = f.ProjectFactory(owner=user, is_private=True) diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index ad28cc16..18562a8e 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -43,7 +43,7 @@ def test_update_milestone_with_userstories_list(client): form_data = { "name": "test", - "user_stories": [UserStorySerializer(us).data] + "user_stories": [{"id": us.id}] } client.login(user) diff --git a/tests/integration/test_models.py b/tests/integration/test_models.py index b0926901..6c6229b1 100644 --- a/tests/integration/test_models.py +++ b/tests/integration/test_models.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 . + import pytest from .. import factories as f diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 33bdabe8..d8943e71 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -42,7 +42,7 @@ from taiga.projects.history.services import take_snapshot from taiga.projects.issues.serializers import IssueSerializer from taiga.projects.userstories.serializers import UserStorySerializer from taiga.projects.tasks.serializers import TaskSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS pytestmark = pytest.mark.django_db @@ -354,6 +354,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai us = f.UserStoryFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -363,6 +364,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -372,6 +374,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -446,6 +449,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): task = f.TaskFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -455,6 +459,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -464,6 +469,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -538,6 +544,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): issue = f.IssueFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -547,6 +554,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -556,6 +564,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -630,6 +639,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) wiki = f.WikiPageFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -639,6 +649,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -648,6 +659,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -778,7 +790,7 @@ def test_watchers_assignation_for_issue(client): assert response.status_code == 400 issue = f.create_issue(project=project1, owner=user1) - data = dict(IssueSerializer(issue).data) + data = {} data["id"] = None data["version"] = None data["watchers"] = [user1.pk, user2.pk] @@ -790,8 +802,7 @@ def test_watchers_assignation_for_issue(client): # Test the impossible case when project is not # exists in create request, and validator works as expected issue = f.create_issue(project=project1, owner=user1) - data = dict(IssueSerializer(issue).data) - + data = {} data["id"] = None data["watchers"] = [user1.pk, user2.pk] data["project"] = None @@ -830,10 +841,11 @@ def test_watchers_assignation_for_task(client): assert response.status_code == 400 task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) - data = dict(TaskSerializer(task).data) - data["id"] = None - data["version"] = None - data["watchers"] = [user1.pk, user2.pk] + data = { + "id": None, + "version": None, + "watchers": [user1.pk, user2.pk] + } url = reverse("tasks-list") response = client.json.post(url, json.dumps(data)) @@ -842,11 +854,11 @@ def test_watchers_assignation_for_task(client): # Test the impossible case when project is not # exists in create request, and validator works as expected task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) - data = dict(TaskSerializer(task).data) - - data["id"] = None - data["watchers"] = [user1.pk, user2.pk] - data["project"] = None + data = { + "id": None, + "watchers": [user1.pk, user2.pk], + "project": None + } url = reverse("tasks-list") response = client.json.post(url, json.dumps(data)) @@ -882,10 +894,11 @@ def test_watchers_assignation_for_us(client): assert response.status_code == 400 us = f.create_userstory(project=project1, owner=user1, status__project=project1) - data = dict(UserStorySerializer(us).data) - data["id"] = None - data["version"] = None - data["watchers"] = [user1.pk, user2.pk] + data = { + "id": None, + "version": None, + "watchers": [user1.pk, user2.pk] + } url = reverse("userstories-list") response = client.json.post(url, json.dumps(data)) @@ -894,11 +907,11 @@ def test_watchers_assignation_for_us(client): # Test the impossible case when project is not # exists in create request, and validator works as expected us = f.create_userstory(project=project1, owner=user1, status__project=project1) - data = dict(UserStorySerializer(us).data) - - data["id"] = None - data["watchers"] = [user1.pk, user2.pk] - data["project"] = None + data = { + "id": None, + "watchers": [user1.pk, user2.pk], + "project": None + } url = reverse("userstories-list") response = client.json.post(url, json.dumps(data)) diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py index 6a494b2f..bc39384f 100644 --- a/tests/integration/test_permissions.py +++ b/tests/integration/test_permissions.py @@ -1,7 +1,25 @@ # -*- 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 . + import pytest -from taiga.permissions import service, permissions +from taiga.permissions import services, choices from django.contrib.auth.models import AnonymousUser from .. import factories @@ -16,15 +34,15 @@ def test_get_user_project_role(): role = factories.RoleFactory() membership = factories.MembershipFactory(user=user1, project=project, role=role) - assert service._get_user_project_membership(user1, project) == membership - assert service._get_user_project_membership(user2, project) is None + assert services._get_user_project_membership(user1, project) == membership + assert services._get_user_project_membership(user2, project) is None def test_anon_get_user_project_permissions(): project = factories.ProjectFactory() project.anon_permissions = ["test1"] project.public_permissions = ["test2"] - assert service.get_user_project_permissions(AnonymousUser(), project) == set(["test1"]) + assert services.get_user_project_permissions(AnonymousUser(), project) == set(["test1"]) def test_user_get_user_project_permissions_on_public_project(): @@ -32,7 +50,7 @@ def test_user_get_user_project_permissions_on_public_project(): project = factories.ProjectFactory() project.anon_permissions = ["test1"] project.public_permissions = ["test2"] - assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"]) + assert services.get_user_project_permissions(user1, project) == set(["test1", "test2"]) def test_user_get_user_project_permissions_on_private_project(): @@ -41,7 +59,7 @@ def test_user_get_user_project_permissions_on_private_project(): project.anon_permissions = ["test1"] project.public_permissions = ["test2"] project.is_private = True - assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"]) + assert services.get_user_project_permissions(user1, project) == set(["test1", "test2"]) def test_owner_get_user_project_permissions(): @@ -56,7 +74,7 @@ def test_owner_get_user_project_permissions(): expected_perms = set( ["test1", "test2", "view_us"] ) - assert service.get_user_project_permissions(user1, project) == expected_perms + assert services.get_user_project_permissions(user1, project) == expected_perms def test_owner_member_get_user_project_permissions(): @@ -69,10 +87,10 @@ def test_owner_member_get_user_project_permissions(): expected_perms = set( ["test1", "test2", "test3"] + - [x[0] for x in permissions.ADMINS_PERMISSIONS] + - [x[0] for x in permissions.MEMBERS_PERMISSIONS] + [x[0] for x in choices.ADMINS_PERMISSIONS] + + [x[0] for x in choices.MEMBERS_PERMISSIONS] ) - assert service.get_user_project_permissions(user1, project) == expected_perms + assert services.get_user_project_permissions(user1, project) == expected_perms def test_member_get_user_project_permissions(): @@ -83,22 +101,22 @@ def test_member_get_user_project_permissions(): role = factories.RoleFactory(permissions=["test3"]) factories.MembershipFactory(user=user1, project=project, role=role) - assert service.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"]) + assert services.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"]) def test_anon_user_has_perm(): project = factories.ProjectFactory() project.anon_permissions = ["test"] - assert service.user_has_perm(AnonymousUser(), "test", project) is True - assert service.user_has_perm(AnonymousUser(), "fail", project) is False + assert services.user_has_perm(AnonymousUser(), "test", project) is True + assert services.user_has_perm(AnonymousUser(), "fail", project) is False def test_authenticated_user_has_perm_on_project(): user1 = factories.UserFactory() project = factories.ProjectFactory() project.public_permissions = ["test"] - assert service.user_has_perm(user1, "test", project) is True - assert service.user_has_perm(user1, "fail", project) is False + assert services.user_has_perm(user1, "test", project) is True + assert services.user_has_perm(user1, "fail", project) is False def test_authenticated_user_has_perm_on_project_related_object(): @@ -107,10 +125,10 @@ def test_authenticated_user_has_perm_on_project_related_object(): project.public_permissions = ["test"] us = factories.UserStoryFactory(project=project) - assert service.user_has_perm(user1, "test", us) is True - assert service.user_has_perm(user1, "fail", us) is False + assert services.user_has_perm(user1, "test", us) is True + assert services.user_has_perm(user1, "fail", us) is False def test_authenticated_user_has_perm_on_invalid_object(): user1 = factories.UserFactory() - assert service.user_has_perm(user1, "test", user1) is False + assert services.user_has_perm(user1, "test", user1) is False diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 9e6279c0..a002aa0f 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.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 django.conf import settings from django.core.files import File @@ -8,8 +26,12 @@ from django.core import signing from taiga.base.utils import json from taiga.projects.services import stats as stats_services from taiga.projects.history.services import take_snapshot -from taiga.permissions.permissions import ANON_PERMISSIONS +from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.models import Project +from taiga.projects.userstories.models import UserStory +from taiga.projects.tasks.models import Task +from taiga.projects.issues.models import Issue +from taiga.projects.epics.models import Epic from taiga.projects.choices import BLOCKED_BY_DELETING from .. import factories as f @@ -604,7 +626,7 @@ def test_projects_user_order(client): #Testing user order url = reverse("projects-list") - url = "%s?member=%s&order_by=memberships__user_order" % (url, user.id) + url = "%s?member=%s&order_by=user_order" % (url, user.id) response = client.json.get(url) response_content = response.data assert response.status_code == 200 @@ -1852,3 +1874,224 @@ def test_delete_project_with_celery_disabled(client, settings): response = client.json.delete(url) assert response.status_code == 204 assert Project.objects.filter(id=project.id).count() == 0 + + +def test_create_tag(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-create-tag", args=(project.id,)) + client.login(user) + data = { + "tag": "newtag", + "color": "#123123" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["newtag", "#123123"]] + + +def test_create_tag_without_color(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-create-tag", args=(project.id,)) + client.login(user) + data = { + "tag": "newtag", + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors[0][0] == "newtag" + + +def test_edit_tag_only_name(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-edit-tag", args=(project.id,)) + client.login(user) + data = { + "from_tag": "tag", + "to_tag": "renamed_tag" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["renamed_tag", "#123123"]] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == ["renamed_tag"] + task = Task.objects.get(id=task.pk) + assert task.tags == ["renamed_tag"] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == ["renamed_tag"] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == ["renamed_tag"] + + +def test_edit_tag_only_color(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-edit-tag", args=(project.id,)) + client.login(user) + data = { + "from_tag": "tag", + "color": "#AAABBB" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["tag", "#AAABBB"]] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == ["tag"] + task = Task.objects.get(id=task.pk) + assert task.tags == ["tag"] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == ["tag"] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == ["tag"] + + +def test_edit_tag(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-edit-tag", args=(project.id,)) + client.login(user) + data = { + "from_tag": "tag", + "to_tag": "renamed_tag", + "color": "#AAABBB" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["renamed_tag", "#AAABBB"]] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == ["renamed_tag"] + task = Task.objects.get(id=task.pk) + assert task.tags == ["renamed_tag"] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == ["renamed_tag"] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == ["renamed_tag"] + + +def test_delete_tag(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-delete-tag", args=(project.id,)) + client.login(user) + data = { + "tag": "tag" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == [] + task = Task.objects.get(id=task.pk) + assert task.tags == [] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == [] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == [] + + +def test_mix_tags(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag1", "#123123"), ("tag2", "#123123"), ("tag3", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag1", "tag3"]) + task = f.TaskFactory.create(project=project, tags=["tag2", "tag3"]) + issue = f.IssueFactory.create(project=project, tags=["tag1", "tag2", "tag3"]) + epic = f.EpicFactory.create(project=project, tags=["tag1", "tag2", "tag3"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-mix-tags", args=(project.id,)) + client.login(user) + data = { + "from_tags": ["tag1", "tag2"], + "to_tag": "tag2" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert set(["tag2", "tag3"]) == set(dict(project.tags_colors).keys()) + user_story = UserStory.objects.get(id=user_story.pk) + assert set(user_story.tags) == set(["tag2", "tag3"]) + task = Task.objects.get(id=task.pk) + assert set(task.tags) == set(["tag2", "tag3"]) + issue = Issue.objects.get(id=issue.pk) + assert set(issue.tags) == set(["tag2", "tag3"]) + epic = Epic.objects.get(id=epic.pk) + assert set(epic.tags) == set(["tag2", "tag3"]) + + +def test_color_tags_project_fired_on_element_create(): + user_story = f.UserStoryFactory.create(tags=["tag"]) + project = Project.objects.get(id=user_story.project.id) + assert project.tags_colors == [["tag", None]] + + +def test_color_tags_project_fired_on_element_update(): + user_story = f.UserStoryFactory.create() + user_story.tags = ["tag"] + user_story.save() + project = Project.objects.get(id=user_story.project.id) + assert ["tag", None] in project.tags_colors + + +def test_color_tags_project_fired_on_element_update_respecting_color(): + project = f.ProjectFactory.create(tags_colors=[["tag", "#123123"]]) + user_story = f.UserStoryFactory.create(project=project) + user_story.tags = ["tag"] + user_story.save() + project = Project.objects.get(id=user_story.project.id) + assert ["tag", "#123123"] in project.tags_colors diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index fb9616cf..bb5e681d 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -23,7 +23,7 @@ from django.core.urlresolvers import reverse from .. import factories as f -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS from tests.utils import disconnect_signals, reconnect_signals @@ -52,37 +52,36 @@ def searches_initial_data(): role__project=m.project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.RoleFactory(project=m.project2) - m.points1 = f.PointsFactory(project=m.project1, value=None) - m.points2 = f.PointsFactory(project=m.project2, value=None) + m.epic11 = f.EpicFactory(project=m.project1, subject="Back to the future") + m.epic12 = f.EpicFactory(project=m.project1, tags=["Back", "future"]) + m.epic13 = f.EpicFactory(project=m.project1) + m.epic14 = f.EpicFactory(project=m.project1, description="Backend to the future") + m.epic21 = f.EpicFactory(project=m.project2, subject="Back to the future") - m.role_points1 = f.RolePointsFactory.create(role=m.project1.roles.all()[0], - points=m.points1, - user_story__project=m.project1) - m.role_points2 = f.RolePointsFactory.create(role=m.project1.roles.all()[0], - points=m.points1, - user_story__project=m.project1, - user_story__description="Back to the future") - m.role_points3 = f.RolePointsFactory.create(role=m.project2.roles.all()[0], - points=m.points2, - user_story__project=m.project2) + m.us11 = f.UserStoryFactory(project=m.project1, subject="Back to the future") + m.us12 = f.UserStoryFactory(project=m.project1, description="Back to the future") + m.us13 = f.UserStoryFactory(project=m.project1, tags=["Backend", "future"]) + m.us14 = f.UserStoryFactory(project=m.project1) + m.us21 = f.UserStoryFactory(project=m.project2, subject="Backend to the future") - m.us1 = m.role_points1.user_story - m.us2 = m.role_points2.user_story - m.us3 = m.role_points3.user_story + m.task11 = f.TaskFactory(project=m.project1, subject="Back to the future") + m.task12 = f.TaskFactory(project=m.project1, tags=["Back", "future"]) + m.task13 = f.TaskFactory(project=m.project1) + m.task14 = f.TaskFactory(project=m.project1, description="Backend to the future") + m.task21 = f.TaskFactory(project=m.project2, subject="Back to the future") - m.tsk1 = f.TaskFactory.create(project=m.project2) - m.tsk2 = f.TaskFactory.create(project=m.project1) - m.tsk3 = f.TaskFactory.create(project=m.project1, subject="Back to the future") + m.issue11 = f.IssueFactory(project=m.project1, description="Back to the future") + m.issue12 = f.IssueFactory(project=m.project1, tags=["back", "future"]) + m.issue13 = f.IssueFactory(project=m.project1) + m.issue14 = f.IssueFactory(project=m.project1, subject="Backend to the future") + m.issue21 = f.IssueFactory(project=m.project2, subject="Back to the future") - m.iss1 = f.IssueFactory.create(project=m.project1, subject="Backend and Frontend") - m.iss2 = f.IssueFactory.create(project=m.project2) - m.iss3 = f.IssueFactory.create(project=m.project1) - - m.wiki1 = f.WikiPageFactory.create(project=m.project1) - m.wiki2 = f.WikiPageFactory.create(project=m.project1, content="Frontend, future") - m.wiki3 = f.WikiPageFactory.create(project=m.project2) + m.wikipage11 = f.WikiPageFactory(project=m.project1) + m.wikipage12 = f.WikiPageFactory(project=m.project1) + m.wikipage13 = f.WikiPageFactory(project=m.project1, content="Backend to the black") + m.wikipage14 = f.WikiPageFactory(project=m.project1, slug="Back to the black") + m.wikipage21 = f.WikiPageFactory(project=m.project2, slug="Backend to the orange") return m @@ -94,11 +93,12 @@ def test_search_all_objects_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id}) assert response.status_code == 200 - assert response.data["count"] == 8 - assert len(response.data["userstories"]) == 2 - assert len(response.data["tasks"]) == 2 - assert len(response.data["issues"]) == 2 - assert len(response.data["wikipages"]) == 2 + assert response.data["count"] == 20 + assert len(response.data["epics"]) == 4 + assert len(response.data["userstories"]) == 4 + assert len(response.data["tasks"]) == 4 + assert len(response.data["issues"]) == 4 + assert len(response.data["wikipages"]) == 4 def test_search_all_objects_in_project_is_not_mine(client, searches_initial_data): @@ -118,20 +118,48 @@ def test_search_text_query_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "future"}) assert response.status_code == 200 - assert response.data["count"] == 3 - assert len(response.data["userstories"]) == 1 - assert len(response.data["tasks"]) == 1 - assert len(response.data["issues"]) == 0 - assert len(response.data["wikipages"]) == 1 + assert response.data["count"] == 12 + assert len(response.data["epics"]) == 3 + assert response.data["epics"][0]["id"] == searches_initial_data.epic11.id + assert response.data["epics"][1]["id"] == searches_initial_data.epic12.id + assert response.data["epics"][2]["id"] == searches_initial_data.epic14.id + assert len(response.data["userstories"]) == 3 + assert response.data["userstories"][0]["id"] == searches_initial_data.us11.id + assert response.data["userstories"][1]["id"] == searches_initial_data.us13.id + assert response.data["userstories"][2]["id"] == searches_initial_data.us12.id + assert len(response.data["tasks"]) == 3 + assert response.data["tasks"][0]["id"] == searches_initial_data.task11.id + assert response.data["tasks"][1]["id"] == searches_initial_data.task12.id + assert response.data["tasks"][2]["id"] == searches_initial_data.task14.id + assert len(response.data["issues"]) == 3 + assert response.data["issues"][0]["id"] == searches_initial_data.issue14.id + assert response.data["issues"][1]["id"] == searches_initial_data.issue12.id + assert response.data["issues"][2]["id"] == searches_initial_data.issue11.id + assert len(response.data["wikipages"]) == 0 response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "back"}) assert response.status_code == 200 - assert response.data["count"] == 3 - assert len(response.data["userstories"]) == 1 - assert len(response.data["tasks"]) == 1 + assert response.data["count"] == 14 + assert len(response.data["epics"]) == 3 + assert response.data["epics"][0]["id"] == searches_initial_data.epic11.id + assert response.data["epics"][1]["id"] == searches_initial_data.epic12.id + assert response.data["epics"][2]["id"] == searches_initial_data.epic14.id + assert len(response.data["userstories"]) == 3 + assert response.data["userstories"][0]["id"] == searches_initial_data.us11.id + assert response.data["userstories"][1]["id"] == searches_initial_data.us13.id + assert response.data["userstories"][2]["id"] == searches_initial_data.us12.id + assert len(response.data["tasks"]) == 3 + assert response.data["tasks"][0]["id"] == searches_initial_data.task11.id + assert response.data["tasks"][1]["id"] == searches_initial_data.task12.id + assert response.data["tasks"][2]["id"] == searches_initial_data.task14.id + assert len(response.data["issues"]) == 3 + assert response.data["issues"][0]["id"] == searches_initial_data.issue14.id + assert response.data["issues"][1]["id"] == searches_initial_data.issue12.id + assert response.data["issues"][2]["id"] == searches_initial_data.issue11.id # Back is a backend substring - assert len(response.data["issues"]) == 1 - assert len(response.data["wikipages"]) == 0 + assert len(response.data["wikipages"]) == 2 + assert response.data["wikipages"][0]["id"] == searches_initial_data.wikipage14.id + assert response.data["wikipages"][1]["id"] == searches_initial_data.wikipage13.id def test_search_text_query_with_an_invalid_project_id(client, searches_initial_data): diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py index e85f22ab..5dfdd3d9 100644 --- a/tests/integration/test_stats.py +++ b/tests/integration/test_stats.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 . + import pytest from .. import factories as f diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index bb077c2f..04546233 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -1,6 +1,28 @@ # -*- 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 . + import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock @@ -67,39 +89,164 @@ def test_create_task_without_default_values(client): assert response.data['status'] == None -def test_api_update_task_tags(client): - project = f.ProjectFactory.create() - task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) - f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) - url = reverse("tasks-detail", kwargs={"pk": task.pk}) - data = {"tags": ["back", "front"], "version": task.version} +def test_api_create_in_bulk_with_status_milestone_userstory(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) - client.login(task.owner) - response = client.json.patch(url, json.dumps(data)) + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) - assert response.status_code == 200, response.data - - -def test_api_create_in_bulk_with_status(client): - us = f.create_userstory() - f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) - us.project.default_task_status = f.TaskStatusFactory.create(project=us.project) url = reverse("tasks-bulk-create") data = { "bulk_tasks": "Story #1\nStory #2", "us_id": us.id, "project_id": us.project.id, - "sprint_id": us.milestone.id, + "milestone_id": us.milestone.id, "status_id": us.project.default_task_status.id } - client.login(us.owner) + client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 200 assert response.data[0]["status"] == us.project.default_task_status.id +def test_api_create_in_bulk_with_status_milestone(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "project_id": us.project.id, + "milestone_id": us.milestone.id, + "status_id": us.project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert response.data[0]["status"] == us.project.default_task_status.id + + +def test_api_create_in_bulk_with_invalid_status(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + + status = f.TaskStatusFactory.create() + + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "milestone_id": milestone.id, + "status_id": status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "status_id" in response.data + + +def test_api_create_in_bulk_with_invalid_milestone(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory() + us = f.create_userstory(project=project) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "milestone_id": milestone.id, + "status_id": project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "milestone_id" in response.data + + +def test_api_create_in_bulk_with_invalid_userstory_1(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory() + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "milestone_id": milestone.id, + "status_id": project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "us_id" in response.data + + +def test_api_create_in_bulk_with_invalid_userstory_2(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": us.project.id, + "milestone_id": milestone.id, + "status_id": us.project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "us_id" in response.data + + def test_api_create_invalid_task(client): # Associated to a milestone and a user story. # But the User Story is not associated with the milestone @@ -143,8 +290,256 @@ def test_api_update_order_in_bulk(client): response1 = client.json.post(url1, json.dumps(data)) response2 = client.json.post(url2, json.dumps(data)) - assert response1.status_code == 204, response1.data - assert response2.status_code == 204, response2.data + assert response1.status_code == 200, response1.data + assert response2.status_code == 200, response2.data + + +def test_api_update_order_in_bulk_invalid_tasks(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task() + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + + +def test_api_update_order_in_bulk_invalid_tasks_for_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project, status=task1.status) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "status_id": task1.status.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_tasks_for_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create(project=project) + task1 = f.create_task(project=project, milestone=mil1) + task2 = f.create_task(project=project, milestone=mil1) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_tasks_for_user_story(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + status = f.TaskStatusFactory.create() + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "status_id": status.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create() + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_user_story_1(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory() + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_user_story_2(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + milestone = f.MilestoneFactory.create(project=project) + us1 = f.create_userstory(project=project) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "milestone_id": milestone.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data def test_get_invalid_csv(client): @@ -180,3 +575,257 @@ def test_custom_fields_csv_generation(): assert row[24] == attr.name row = next(reader) assert row[24] == "val1" + + +def test_get_tasks_including_attachments(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + task = f.TaskFactory.create(project=project) + f.TaskAttachmentFactory(project=project, content_object=task) + url = reverse("tasks-list") + + client.login(project.owner) + + response = client.get(url) + assert response.status_code == 200 + assert response.data[0].get("attachments") == [] + + url = reverse("tasks-list") + "?include_attachments=1" + response = client.get(url) + assert response.status_code == 200 + assert len(response.data[0].get("attachments")) == 1 + + +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user, subject="test") + + url = reverse("tasks-list") + "?created_date=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == task.subject + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user, subject="test") + + url = reverse("tasks-list") + "?created_date__lt=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["subject"] == old_task.subject + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user) + + url = reverse("tasks-list") + "?created_date__lte=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + _day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + older_task = f.create_task(owner=user) + task = f.create_task(owner=user, subject="test") + # we have to refresh as it slightly differs + task.refresh_from_db() + + assert older_task.modified_date < task.modified_date + + url = reverse("tasks-list") + "?modified_date__gte=%s" % ( + quote(task.modified_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == task.subject + + +def test_api_filter_by_finished_date(client): + user = f.UserFactory(is_superuser=True) + project = f.ProjectFactory.create() + status0 = f.TaskStatusFactory.create(project=project, is_closed=True) + + task = f.create_task(owner=user) + finished_task = f.create_task(owner=user, status=status0, subject="test") + + assert finished_task.finished_date + + url = reverse("tasks-list") + "?finished_date__gte=%s" % ( + quote(finished_task.finished_date.isoformat()) + ) + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == finished_task.subject + + +def test_api_filters_data(client): + project = f.ProjectFactory.create() + user1 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user1, project=project) + user2 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user2, project=project) + user3 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user3, project=project) + + status0 = f.TaskStatusFactory.create(project=project) + status1 = f.TaskStatusFactory.create(project=project) + status2 = f.TaskStatusFactory.create(project=project) + status3 = f.TaskStatusFactory.create(project=project) + + tag0 = "test1test2test3" + tag1 = "test1" + tag2 = "test2" + tag3 = "test3" + + # ------------------------------------------------------ + # | Task | Owner | Assigned To | Tags | + # |-------#--------#-------------#---------------------| + # | 0 | user2 | None | tag1 | + # | 1 | user1 | None | tag2 | + # | 2 | user3 | None | tag1 tag2 | + # | 3 | user2 | None | tag3 | + # | 4 | user1 | user1 | tag1 tag2 tag3 | + # | 5 | user3 | user1 | tag3 | + # | 6 | user2 | user1 | tag1 tag2 | + # | 7 | user1 | user2 | tag3 | + # | 8 | user3 | user2 | tag1 | + # | 9 | user2 | user3 | tag0 | + # ------------------------------------------------------ + + task0 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + task1 = f.TaskFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + task2 = f.TaskFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + task3 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + task4 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + task5 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + task6 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + task7 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + task8 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + task9 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) + + url = reverse("tasks-filters-data") + "?project={}".format(project.id) + + client.login(user1) + + ## No filter + response = client.get(url) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 5 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + + ## Filter ((status0 or status3) + response = client.get(url + "&status={},{}".format(status3.id, status0.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 + + ## Filter ((tag1 and tag2) and (user1 or user2)) + response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 diff --git a/tests/integration/test_tasks_tags.py b/tests/integration/test_tasks_tags.py new file mode 100644 index 00000000..60856688 --- /dev/null +++ b/tests/integration/test_tasks_tags.py @@ -0,0 +1,161 @@ +# -*- 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 unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_task_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) + url = reverse("tasks-detail", kwargs={"pk": task.pk}) + data = { + "tags": [], + "version": task.version + } + + client.login(task.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_task_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) + url = reverse("tasks-detail", kwargs={"pk": task.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": task.version + } + + client.login(task.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_task_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) + url = reverse("tasks-detail", kwargs={"pk": task.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": task.version + } + + client.login(task.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_task_with_tags(client): + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) + status = f.TaskStatusFactory.create(project=project) + project.default_task_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("tasks-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", "#bbbbbb"], + ["ux", None] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + task_tags_colors = OrderedDict(response.data["tags"]) + + assert task_tags_colors["back"] == "#fff8e7" + assert task_tags_colors["front"] == "#aaaaaa" + assert task_tags_colors["ux"] == "#fabada" + + tags_colors = OrderedDict(project.tags_colors) + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_totals_projects.py b/tests/integration/test_totals_projects.py index e46c1b21..8d29d950 100644 --- a/tests/integration/test_totals_projects.py +++ b/tests/integration/test_totals_projects.py @@ -39,6 +39,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime us = f.UserStoryFactory.create(project=project, owner=project.owner) f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, @@ -57,6 +58,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, @@ -75,6 +77,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, @@ -93,6 +96,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 9277d821..3accebb7 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.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 . + import pytest from tempfile import NamedTemporaryFile @@ -12,10 +30,11 @@ from ..utils import DUMMY_BMP_DATA from taiga.base.utils import json from taiga.base.utils.thumbnails import get_thumbnail_url +from taiga.base.utils.dicts import into_namedtuple from taiga.users import models from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer from taiga.auth.tokens import get_token_for_user -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.users.services import get_watched_list, get_voted_list, get_liked_list from taiga.projects.notifications.choices import NotifyLevel @@ -80,6 +99,35 @@ def test_update_user_with_invalid_email(client): assert response.data['_error_message'] == 'Not valid email' +def test_update_user_with_unallowed_domain_email(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + user = f.UserFactory.create(email="my@email.com") + url = reverse('users-detail', kwargs={"pk": user.pk}) + data = {"email": "my@invalid-email.com"} + + client.login(user) + response = client.patch(url, json.dumps(data), content_type="application/json") + + assert response.status_code == 400 + assert response.data['_error_message'] == 'Not valid email' + + +def test_update_user_with_allowed_domain_email(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + + user = f.UserFactory.create(email="old@email.com") + url = reverse('users-detail', kwargs={"pk": user.pk}) + data = {"email": "new@email.com"} + + client.login(user) + response = client.patch(url, json.dumps(data), content_type="application/json") + + assert response.status_code == 200 + user = models.User.objects.get(pk=user.id) + assert user.email_token is not None + assert user.new_email == "new@email.com" + + def test_update_user_with_valid_email(client): user = f.UserFactory.create(email="old@email.com") url = reverse('users-detail', kwargs={"pk": user.pk}) @@ -341,7 +389,7 @@ def test_list_contacts_no_projects(client): def test_list_contacts_public_projects(client): project = f.ProjectFactory.create(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS))) + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) user_1 = f.UserFactory.create() user_2 = f.UserFactory.create() @@ -406,6 +454,9 @@ def test_get_watched_list(): membership = f.MembershipFactory(project=project, role=role, user=fav_user) project.add_watcher(fav_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + epic.add_watcher(fav_user) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") user_story.add_watcher(fav_user) @@ -415,11 +466,12 @@ def test_get_watched_list(): issue = f.IssueFactory(project=project, subject="Testing issue") issue.add_watcher(fav_user) - assert len(get_watched_list(fav_user, viewer_user)) == 4 + assert len(get_watched_list(fav_user, viewer_user)) == 5 assert len(get_watched_list(fav_user, viewer_user, type="project")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="userstory")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="task")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="issue")) == 1 + assert len(get_watched_list(fav_user, viewer_user, type="epic")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="unknown")) == 0 assert len(get_watched_list(fav_user, viewer_user, q="issue")) == 1 @@ -452,6 +504,11 @@ def test_get_voted_list(): role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=fav_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + content_type = ContentType.objects.get_for_model(epic) + f.VoteFactory(content_type=content_type, object_id=epic.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=epic.id, count=1) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") content_type = ContentType.objects.get_for_model(user_story) f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user) @@ -467,7 +524,8 @@ def test_get_voted_list(): f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user) f.VotesFactory(content_type=content_type, object_id=issue.id, count=1) - assert len(get_voted_list(fav_user, viewer_user)) == 3 + assert len(get_voted_list(fav_user, viewer_user)) == 4 + assert len(get_voted_list(fav_user, viewer_user, type="epic")) == 1 assert len(get_voted_list(fav_user, viewer_user, type="userstory")) == 1 assert len(get_voted_list(fav_user, viewer_user, type="task")) == 1 assert len(get_voted_list(fav_user, viewer_user, type="issue")) == 1 @@ -481,13 +539,13 @@ def test_get_watched_list_valid_info_for_project(): fav_user = f.UserFactory() viewer_user = f.UserFactory() - project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) project.add_watcher(fav_user) raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0] - project_watch_info = LikedObjectSerializer(raw_project_watch_info).data + project_watch_info = LikedObjectSerializer(into_namedtuple(raw_project_watch_info)).data assert project_watch_info["type"] == "project" assert project_watch_info["id"] == project.id @@ -499,11 +557,6 @@ def test_get_watched_list_valid_info_for_project(): assert project_watch_info["assigned_to"] == None assert project_watch_info["status"] == None assert project_watch_info["status_color"] == None - - tags_colors = {tc["name"]:tc["color"] for tc in project_watch_info["tags_colors"]} - assert "test" in tags_colors - assert "tag" in tags_colors - assert project_watch_info["is_private"] == project.is_private assert project_watch_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL) assert project_watch_info["is_fan"] == False @@ -515,9 +568,8 @@ def test_get_watched_list_valid_info_for_project(): assert project_watch_info["project_slug"] == None assert project_watch_info["project_is_private"] == None assert project_watch_info["project_blocked_code"] == None - assert project_watch_info["assigned_to_username"] == None - assert project_watch_info["assigned_to_full_name"] == None - assert project_watch_info["assigned_to_photo"] == None + assert project_watch_info["assigned_to"] == None + assert project_watch_info["assigned_to_extra_info"] == None def test_get_watched_list_for_project_with_ignored_notify_level(): @@ -526,7 +578,7 @@ def test_get_watched_list_for_project_with_ignored_notify_level(): viewer_user = f.UserFactory() project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=fav_user) notify_policy = NotifyPolicy.objects.get(user=fav_user, project=project) notify_policy.notify_level=NotifyLevel.none @@ -540,13 +592,13 @@ def test_get_liked_list_valid_info(): fan_user = f.UserFactory() viewer_user = f.UserFactory() - project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) + project = f.ProjectFactory(is_private=False, name="Testing project") content_type = ContentType.objects.get_for_model(project) like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user) project.refresh_totals() raw_project_like_info = get_liked_list(fan_user, viewer_user)[0] - project_like_info = LikedObjectSerializer(raw_project_like_info).data + project_like_info = LikedObjectSerializer(into_namedtuple(raw_project_like_info)).data assert project_like_info["type"] == "project" assert project_like_info["id"] == project.id @@ -558,11 +610,6 @@ def test_get_liked_list_valid_info(): assert project_like_info["assigned_to"] == None assert project_like_info["status"] == None assert project_like_info["status_color"] == None - - tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]} - assert "test" in tags_colors - assert "tag" in tags_colors - assert project_like_info["is_private"] == project.is_private assert project_like_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL) @@ -575,9 +622,8 @@ def test_get_liked_list_valid_info(): assert project_like_info["project_slug"] == None assert project_like_info["project_is_private"] == None assert project_like_info["project_blocked_code"] == None - assert project_like_info["assigned_to_username"] == None - assert project_like_info["assigned_to_full_name"] == None - assert project_like_info["assigned_to_photo"] == None + assert project_like_info["assigned_to"] == None + assert project_like_info["assigned_to_extra_info"] == None def test_get_watched_list_valid_info_for_not_project_types(): @@ -588,6 +634,7 @@ def test_get_watched_list_valid_info_for_not_project_types(): project = f.ProjectFactory(is_private=False, name="Testing project") factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -601,7 +648,7 @@ def test_get_watched_list_valid_info_for_not_project_types(): instance.add_watcher(fav_user) raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0] - instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data + instance_watch_info = VotedObjectSerializer(into_namedtuple(raw_instance_watch_info)).data assert instance_watch_info["type"] == object_type assert instance_watch_info["id"] == instance.id @@ -629,9 +676,11 @@ def test_get_watched_list_valid_info_for_not_project_types(): assert instance_watch_info["project_slug"] == instance.project.slug assert instance_watch_info["project_is_private"] == instance.project.is_private assert instance_watch_info["project_blocked_code"] == instance.project.blocked_code - assert instance_watch_info["assigned_to_username"] == instance.assigned_to.username - assert instance_watch_info["assigned_to_full_name"] == instance.assigned_to.full_name - assert instance_watch_info["assigned_to_photo"] != "" + assert instance_watch_info["assigned_to"] != None + assert instance_watch_info["assigned_to_extra_info"]["username"] == instance.assigned_to.username + assert instance_watch_info["assigned_to_extra_info"]["full_name_display"] == instance.assigned_to.get_full_name() + assert instance_watch_info["assigned_to_extra_info"]["photo"] == None + assert instance_watch_info["assigned_to_extra_info"]["gravatar_id"] != None def test_get_voted_list_valid_info(): @@ -642,6 +691,7 @@ def test_get_voted_list_valid_info(): project = f.ProjectFactory(is_private=False, name="Testing project") factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -658,7 +708,7 @@ def test_get_voted_list_valid_info(): f.VotesFactory(content_type=content_type, object_id=instance.id, count=3) raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0] - instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data + instance_vote_info = VotedObjectSerializer(into_namedtuple(raw_instance_vote_info)).data assert instance_vote_info["type"] == object_type assert instance_vote_info["id"] == instance.id @@ -686,9 +736,12 @@ def test_get_voted_list_valid_info(): assert instance_vote_info["project_slug"] == instance.project.slug assert instance_vote_info["project_is_private"] == instance.project.is_private assert instance_vote_info["project_blocked_code"] == instance.project.blocked_code - assert instance_vote_info["assigned_to_username"] == instance.assigned_to.username - assert instance_vote_info["assigned_to_full_name"] == instance.assigned_to.full_name - assert instance_vote_info["assigned_to_photo"] != "" + assert instance_vote_info["assigned_to"] != None + assert instance_vote_info["assigned_to_extra_info"]["username"] == instance.assigned_to.username + assert instance_vote_info["assigned_to_extra_info"]["full_name_display"] == instance.assigned_to.get_full_name() + assert instance_vote_info["assigned_to_extra_info"]["photo"] == None + assert instance_vote_info["assigned_to_extra_info"]["gravatar_id"] != None + def test_get_watched_list_with_liked_and_voted_objects(client): @@ -702,6 +755,7 @@ def test_get_watched_list_with_liked_and_voted_objects(client): f.LikeFactory(content_type=content_type, object_id=project.id, user=fav_user) voted_elements_factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -752,6 +806,7 @@ def test_get_voted_list_with_watched_objects(client): membership = f.MembershipFactory(project=project, role=role, user=fav_user) voted_elements_factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -779,9 +834,12 @@ def test_get_watched_list_permissions(): project = f.ProjectFactory(is_private=True, name="Testing project") project.add_watcher(fav_user) - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + epic.add_watcher(fav_user) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") user_story.add_watcher(fav_user) @@ -797,13 +855,13 @@ def test_get_watched_list_permissions(): #If the project is private but the viewer user has permissions the votes should # be accesible - assert len(get_watched_list(fav_user, viewer_priviliged_user)) == 4 + assert len(get_watched_list(fav_user, viewer_priviliged_user)) == 5 #If the project is private but has the required anon permissions the votes should # be accesible by any user too - project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.anon_permissions = ["view_project", "view_epic", "view_us", "view_tasks", "view_issues"] project.save() - assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 4 + assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 5 def test_get_liked_list_permissions(): @@ -838,9 +896,14 @@ def test_get_voted_list_permissions(): viewer_priviliged_user = f.UserFactory() project = f.ProjectFactory(is_private=True, name="Testing project") - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + content_type = ContentType.objects.get_for_model(epic) + f.VoteFactory(content_type=content_type, object_id=epic.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=epic.id, count=1) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") content_type = ContentType.objects.get_for_model(user_story) f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user) @@ -862,10 +925,10 @@ def test_get_voted_list_permissions(): #If the project is private but the viewer user has permissions the votes should # be accesible - assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 3 + assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 4 #If the project is private but has the required anon permissions the votes should # be accesible by any user too - project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.anon_permissions = ["view_project", "view_epic", "view_us", "view_tasks", "view_issues"] project.save() - assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 3 + assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 4 diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 0cf37bb6..5d7bec2a 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -1,14 +1,34 @@ # -*- coding: utf-8 -*- -import copy +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.userstories import services, models -from taiga.projects.userstories.serializers import UserStorySerializer from .. import factories as f @@ -34,17 +54,16 @@ def test_create_userstories_in_bulk(): def test_update_userstories_order_in_bulk(): - data = [{"us_id": 1, "order": 1}, {"us_id": 2, "order": 2}] - - project = mock.Mock() - project.pk = 1 + project = f.ProjectFactory.create() + us1 = f.UserStoryFactory.create(project=project, backlog_order=1) + us2 = f.UserStoryFactory.create(project=project, backlog_order=2) + data = [{"us_id": us1.id, "order": 1}, {"us_id": us2.id, "order": 2}] with mock.patch("taiga.projects.userstories.services.db") as db: services.update_userstories_order_in_bulk(data, "backlog_order", project) - db.update_in_bulk_with_ids.assert_called_once_with([1, 2], - [{"backlog_order": 1}, - {"backlog_order": 2}], - model=models.UserStory) + db.update_attr_in_bulk_for_ids.assert_called_once_with({us1.id: 1, us2.id: 2}, + "backlog_order", + models.UserStory) def test_create_userstory_with_watchers(client): @@ -90,7 +109,7 @@ def test_create_userstory_without_default_values(client): client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert response.data['status'] == None + assert response.data['status'] is None def test_api_delete_userstory(client): @@ -138,6 +157,24 @@ def test_api_create_in_bulk_with_status(client): assert response.data[0]["status"] == project.default_us_status.id +def test_api_create_in_bulk_with_invalid_status(client): + project = f.create_project() + status = f.UserStoryStatusFactory.create() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("userstories-bulk-create") + data = { + "bulk_stories": "Story #1\nStory #2", + "project_id": project.id, + "status_id": status.id + } + + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400, response.data + assert "status_id" in response.data + + def test_api_update_orders_in_bulk(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) @@ -156,13 +193,196 @@ def test_api_update_orders_in_bulk(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) - response3 = client.json.post(url3, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 200, response.data - assert response1.status_code == 204, response1.data - assert response2.status_code == 204, response2.data - assert response3.status_code == 204, response3.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 200, response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 200, response.data + + +def test_api_update_orders_in_bulk_invalid_userstories(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + us3 = f.create_userstory() + + url1 = reverse("userstories-bulk-update-backlog-order") + url2 = reverse("userstories-bulk-update-kanban-order") + url3 = reverse("userstories-bulk-update-sprint-order") + + data = { + "project_id": project.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}, + {"us_id": us3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_stories" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_stories" in response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_stories" in response.data + + +def test_api_update_orders_in_bulk_invalid_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + status = f.UserStoryStatusFactory.create() + us1 = f.create_userstory(project=project, status=status) + us2 = f.create_userstory(project=project, status=us1.status) + us3 = f.create_userstory(project=project) + + url1 = reverse("userstories-bulk-update-backlog-order") + url2 = reverse("userstories-bulk-update-kanban-order") + url3 = reverse("userstories-bulk-update-sprint-order") + + data = { + "project_id": project.id, + "status_id": status.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}, + {"us_id": us3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_stories" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_stories" in response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_stories" in response.data + + +def test_api_update_orders_in_bulk_invalid_milestione(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create() + us1 = f.create_userstory(project=project, milestone=mil1) + us2 = f.create_userstory(project=project, milestone=mil1) + us3 = f.create_userstory(project=project) + + url1 = reverse("userstories-bulk-update-backlog-order") + url2 = reverse("userstories-bulk-update-kanban-order") + url3 = reverse("userstories-bulk-update-sprint-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}, + {"us_id": us3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_stories" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_stories" in response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_stories" in response.data + + +def test_api_update_milestone_in_bulk(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + milestone = f.MilestoneFactory.create(project=project) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + us3 = f.create_userstory(project=project, milestone=milestone, sprint_order=1) + us4 = f.create_userstory(project=project, milestone=milestone, sprint_order=2) + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": milestone.id, + "bulk_stories": [{"us_id": us1.id, "order": 2}, + {"us_id": us2.id, "order": 3}] + } + + client.login(project.owner) + + assert project.milestones.get(id=milestone.id).user_stories.count() == 2 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 204, response.data + assert project.milestones.get(id=milestone.id).user_stories.count() == 4 + assert list(project.milestones.get(id=milestone.id).\ + user_stories.\ + order_by("sprint_order").\ + values_list("id", "sprint_order")) == [(us3.id, 1), (us1.id, 2), (us2.id,3), (us4.id,4)] + + +def test_api_update_milestone_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + m2 = f.MilestoneFactory.create() + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": m2.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert "milestone_id" in response.data + + +def test_api_update_milestone_in_bulk_invalid_userstories(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory() + milestone = f.MilestoneFactory.create(project=project) + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": milestone.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert "bulk_stories" in response.data def test_update_userstory_points(client): @@ -176,48 +396,53 @@ def test_update_userstory_points(client): f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project, user=user2, role=role2) - f.PointsFactory.create(project=project, value=None) - f.PointsFactory.create(project=project, value=1) + points1 = f.PointsFactory.create(project=project, value=None) + points2 = f.PointsFactory.create(project=project, value=1) points3 = f.PointsFactory.create(project=project, value=2) - us = f.UserStoryFactory.create(project=project,owner=user1, status__project=project, + us = f.UserStoryFactory.create(project=project, owner=user1, status__project=project, milestone__project=project) - usdata = UserStorySerializer(us).data url = reverse("userstories-detail", args=[us.pk]) client.login(user1) # invalid role - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({"222222": points3.pk}) + data = { + "version": us.version, + "points": { + str(role1.pk): points1.pk, + str(role2.pk): points2.pk, + "222222": points3.pk + } + } response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 # invalid point - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({str(role1.pk): "999999"}) + data = { + "version": us.version, + "points": { + str(role1.pk): 999999, + str(role2.pk): points2.pk + } + } response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 # Api should save successful - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({str(role1.pk): points3.pk}) + data = { + "version": us.version, + "points": { + str(role1.pk): points3.pk, + str(role2.pk): points2.pk + } + } response = client.json.patch(url, json.dumps(data)) - us = models.UserStory.objects.get(pk=us.pk) - usdatanew = UserStorySerializer(us).data - assert response.status_code == 200, str(response.content) - assert response.data["points"] == usdatanew['points'] - assert response.data["points"] != usdata['points'] + assert response.data["points"][str(role1.pk)] == points3.pk def test_update_userstory_rolepoints_on_add_new_role(client): @@ -318,6 +543,112 @@ def test_get_total_points(client): assert us_mixed.get_total_points() == 1.0 +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_userstory = f.create_userstory(owner=user, created_date=one_day_ago) + userstory = f.create_userstory(owner=user, subject="test") + + url = reverse("userstories-list") + "?created_date=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory.subject + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_userstory = f.create_userstory( + owner=user, created_date=one_day_ago, subject="old test" + ) + userstory = f.create_userstory(owner=user) + + url = reverse("userstories-list") + "?created_date__lt=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["subject"] == old_userstory.subject + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_userstory = f.create_userstory(owner=user, created_date=one_day_ago) + userstory = f.create_userstory(owner=user) + + url = reverse("userstories-list") + "?created_date__lte=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + + older_userstory = f.create_userstory(owner=user) + userstory = f.create_userstory(owner=user, subject="test") + # we have to refresh as it slightly differs + userstory.refresh_from_db() + + assert older_userstory.modified_date < userstory.modified_date + + url = reverse("userstories-list") + "?modified_date__gte=%s" % ( + quote(userstory.modified_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory.subject + + +def test_api_filter_by_finish_date(client): + user = f.UserFactory(is_superuser=True) + one_day_later = datetime.now(pytz.utc) + timedelta(days=1) + + userstory = f.create_userstory(owner=user) + userstory_to_finish = f.create_userstory( + owner=user, finish_date=one_day_later, subject="test" + ) + + assert userstory_to_finish.finish_date + + url = reverse("userstories-list") + "?finish_date__gte=%s" % ( + quote(userstory_to_finish.finish_date.isoformat()) + ) + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory_to_finish.subject + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) @@ -332,52 +663,62 @@ def test_api_filters_data(client): status2 = f.UserStoryStatusFactory.create(project=project) status3 = f.UserStoryStatusFactory.create(project=project) + epic0 = f.EpicFactory.create(project=project) + epic1 = f.EpicFactory.create(project=project) + epic2 = f.EpicFactory.create(project=project) + tag0 = "test1test2test3" tag1 = "test1" tag2 = "test2" tag3 = "test3" - # ------------------------------------------------------ - # | US | Owner | Assigned To | Tags | - # |-------#--------#-------------#---------------------| - # | 0 | user2 | None | tag1 | - # | 1 | user1 | None | tag2 | - # | 2 | user3 | None | tag1 tag2 | - # | 3 | user2 | None | tag3 | - # | 4 | user1 | user1 | tag1 tag2 tag3 | - # | 5 | user3 | user1 | tag3 | - # | 6 | user2 | user1 | tag1 tag2 | - # | 7 | user1 | user2 | tag3 | - # | 8 | user3 | user2 | tag1 | - # | 9 | user2 | user3 | tag0 | - # ------------------------------------------------------ + # ------------------------------------------------------------------------------ + # | US | Status | Owner | Assigned To | Tags | Epic | + # |-------#---------#--------#-------------#---------------------#-------------- + # | 0 | status3 | user2 | None | tag1 | epic0 | + # | 1 | status3 | user1 | None | tag2 | None | + # | 2 | status1 | user3 | None | tag1 tag2 | epic1 | + # | 3 | status0 | user2 | None | tag3 | None | + # | 4 | status0 | user1 | user1 | tag1 tag2 tag3 | epic0 | + # | 5 | status2 | user3 | user1 | tag3 | None | + # | 6 | status3 | user2 | user1 | tag1 tag2 | epic0 epic2 | + # | 7 | status0 | user1 | user2 | tag3 | None | + # | 8 | status3 | user3 | user2 | tag1 | epic2 | + # | 9 | status1 | user2 | user3 | tag0 | None | + # ------------------------------------------------------------------------------ - user_story0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, - status=status3, tags=[tag1]) - user_story1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, - status=status3, tags=[tag2]) - user_story2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, - status=status1, tags=[tag1, tag2]) - user_story3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, - status=status0, tags=[tag3]) - user_story4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, - status=status0, tags=[tag1, tag2, tag3]) - user_story5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, - status=status2, tags=[tag3]) - user_story6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, - status=status3, tags=[tag1, tag2]) - user_story7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, - status=status0, tags=[tag3]) - user_story8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, - status=status3, tags=[tag1]) - user_story9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, - status=status1, tags=[tag0]) + us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + f.RelatedUserStory.create(user_story=us0, epic=epic0) + us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + f.RelatedUserStory.create(user_story=us2, epic=epic1) + us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + f.RelatedUserStory.create(user_story=us4, epic=epic0) + us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + f.RelatedUserStory.create(user_story=us6, epic=epic0) + f.RelatedUserStory.create(user_story=us6, epic=epic2) + us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + f.RelatedUserStory.create(user_story=us8, epic=epic2) + us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) url = reverse("userstories-filters-data") + "?project={}".format(project.id) client.login(user1) - ## No filter + # No filter response = client.get(url) assert response.status_code == 200 @@ -385,7 +726,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 4 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 @@ -400,7 +741,12 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 - ## Filter ((status0 or status3) + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + + # Filter ((status0 or status3) response = client.get(url + "&status={},{}".format(status3.id, status0.id)) assert response.status_code == 200 @@ -408,7 +754,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 @@ -418,13 +764,17 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 - ## Filter ((tag1 and tag2) and (user1 or user2)) + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + + # Filter ((tag1 and tag2) and (user1 or user2)) response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) assert response.status_code == 200 @@ -432,7 +782,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 0 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 @@ -442,12 +792,44 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 1 + + # Filter (epic0 epic2) + response = client.get(url + "&epic={},{}".format(epic0.id, epic2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 3 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + def test_get_invalid_csv(client): url = reverse("userstories-csv") @@ -472,7 +854,7 @@ def test_custom_fields_csv_generation(): attr = f.UserStoryCustomAttributeFactory.create(project=project, name="attr1", description="desc") us = f.UserStoryFactory.create(project=project) attr_values = us.custom_attributes_values - attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.attributes_values = {str(attr.id): "val1"} attr_values.save() queryset = project.user_stories.all() data = services.userstories_to_csv(project, queryset) @@ -511,7 +893,7 @@ def test_update_userstory_update_watchers(client): client.login(user=us.owner) url = reverse("userstories-detail", kwargs={"pk": us.pk}) - data = {"watchers": [watching_user.id], "version":1} + data = {"watchers": [watching_user.id], "version": 1} response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -530,7 +912,7 @@ def test_update_userstory_remove_watchers(client): client.login(user=us.owner) url = reverse("userstories-detail", kwargs={"pk": us.pk}) - data = {"watchers": [], "version":1} + data = {"watchers": [], "version": 1} response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -550,7 +932,7 @@ def test_update_userstory_update_tribe_gig(client): "id": 2, "title": "This is a gig test title" }, - "version":1 + "version": 1 } client.login(user=us.owner) @@ -558,3 +940,45 @@ def test_update_userstory_update_tribe_gig(client): assert response.status_code == 200 assert response.data["tribe_gig"] == data["tribe_gig"] + + +def test_get_user_stories_including_tasks(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + user_story = f.UserStoryFactory.create(project=project) + f.TaskFactory.create(user_story=user_story) + url = reverse("userstories-list") + + client.login(project.owner) + + response = client.get(url) + assert response.status_code == 200 + assert response.data[0].get("tasks") == [] + + url = reverse("userstories-list") + "?include_tasks=1" + response = client.get(url) + assert response.status_code == 200 + assert len(response.data[0].get("tasks")) == 1 + + +def test_get_user_stories_including_attachments(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + user_story = f.UserStoryFactory.create(project=project) + f.UserStoryAttachmentFactory(project=project, content_object=user_story) + url = reverse("userstories-list") + + client.login(project.owner) + + response = client.get(url) + assert response.status_code == 200 + assert response.data[0].get("attachments") == [] + + url = reverse("userstories-list") + "?include_attachments=1" + response = client.get(url) + assert response.status_code == 200 + assert len(response.data[0].get("attachments")) == 1 diff --git a/tests/integration/test_userstories_tags.py b/tests/integration/test_userstories_tags.py new file mode 100644 index 00000000..d89c172f --- /dev/null +++ b/tests/integration/test_userstories_tags.py @@ -0,0 +1,161 @@ +# -*- 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 unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_user_story_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + user_story = f.create_userstory(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True) + url = reverse("userstories-detail", kwargs={"pk": user_story.pk}) + data = { + "tags": [], + "version": user_story.version + } + + client.login(user_story.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_user_story_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + user_story = f.create_userstory(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True) + url = reverse("userstories-detail", kwargs={"pk": user_story.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": user_story.version + } + + client.login(user_story.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_user_story_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + user_story = f.create_userstory(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True) + url = reverse("userstories-detail", kwargs={"pk": user_story.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": user_story.version + } + + client.login(user_story.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_user_story_with_tags(client): + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) + status = f.UserStoryStatusFactory.create(project=project) + project.default_userstory_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("userstories-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", "#bbbbbb"], + ["ux", None] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + us_tags_colors = OrderedDict(response.data["tags"]) + + assert us_tags_colors["back"] == "#fff8e7" + assert us_tags_colors["front"] == "#aaaaaa" + assert us_tags_colors["ux"] == "#fabada" + + tags_colors = OrderedDict(project.tags_colors) + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py index a5d4968b..f6caafb3 100644 --- a/tests/integration/test_watch_projects.py +++ b/tests/integration/test_watch_projects.py @@ -21,7 +21,7 @@ import pytest import json from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from .. import factories as f @@ -130,7 +130,7 @@ def test_get_project_is_watcher(client): user = f.UserFactory.create() project = f.ProjectFactory.create(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS))) + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) url_detail = reverse("projects-detail", args=(project.id,)) url_watch = reverse("projects-watch", args=(project.id,)) diff --git a/tests/integration/test_webhooks_epics.py b/tests/integration/test_webhooks_epics.py new file mode 100644 index 00000000..f77dc829 --- /dev/null +++ b/tests/integration/test_webhooks_epics.py @@ -0,0 +1,319 @@ +# -*- 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 . + +import pytest +from unittest.mock import patch + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +def test_webhooks_when_create_epic(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_epic(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.subject = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["subject"] == obj.subject + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + + +def test_webhooks_when_delete_epic(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data + + +def test_webhooks_when_update_epic_attachments(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create attachments + attachment1 = f.EpicAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + attachment2 = f.EpicAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 2 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Update attachment + attachment1.description = "new attachment description" + attachment1.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 1 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Delete attachment + attachment2.delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 1 + + +def test_webhooks_when_update_epic_custom_attributes(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=obj.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=obj.project) + ct2_id = "{}".format(custom_attr_2.id) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create custom attributes + obj.custom_attributes_values.attributes_values = { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + } + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 2 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Update custom attributes + obj.custom_attributes_values.attributes_values[ct1_id] = "test_2_updated" + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 1 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Delete custom attributes + del obj.custom_attributes_values.attributes_values[ct1_id] + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 1 + + +def test_webhooks_when_create_epic_related_userstory(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + epic = f.EpicFactory.create(project=project) + obj = f.RelatedUserStory.create(epic=epic) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "relateduserstory" + assert data["by"]["id"] == epic.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_epic_related_userstory(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + epic = f.EpicFactory.create(project=project) + obj = f.RelatedUserStory.create(epic=epic, order=33) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner) + assert send_request_mock.call_count == 2 + + obj.order = 66 + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "relateduserstory" + assert data["by"]["id"] == epic.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["order"] == obj.order + assert data["change"]["diff"]["order"]["to"] == 66 + assert data["change"]["diff"]["order"]["from"] == 33 + + +def test_webhooks_when_delete_epic_related_userstory(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + epic = f.EpicFactory.create(project=project) + obj = f.RelatedUserStory.create(epic=epic, order=33) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "relateduserstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py index 491ec5b4..8789408d 100644 --- a/tests/integration/test_webhooks_issues.py +++ b/tests/integration/test_webhooks_issues.py @@ -19,7 +19,6 @@ import pytest from unittest.mock import patch -from unittest.mock import Mock from .. import factories as f @@ -29,8 +28,6 @@ from taiga.projects.history import services pytestmark = pytest.mark.django_db(transaction=True) -from taiga.base.utils import json - def test_webhooks_when_create_issue(settings): settings.WEBHOOKS_ENABLED = True project = f.ProjectFactory() @@ -79,7 +76,7 @@ def test_webhooks_when_update_issue(settings): assert data["data"]["subject"] == obj.subject assert data["change"]["comment"] == "test_comment" assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] - assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] def test_webhooks_when_delete_issue(settings): diff --git a/tests/integration/test_wikilinks.py b/tests/integration/test_wikilinks.py new file mode 100644 index 00000000..20e185dc --- /dev/null +++ b/tests/integration/test_wikilinks.py @@ -0,0 +1,118 @@ +# -*- 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 django.core import mail + +from taiga.base.utils import json +from taiga.projects.notifications.choices import NotifyLevel + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_create_wiki_link_of_existent_wiki_page_with_permissions(client): + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_wiki_pages', 'view_wiki_link', + 'add_wiki_page', 'add_wiki_link']) + + f.MembershipFactory.create(project=project, user=project.owner, role=role) + project.owner.notify_policies.filter(project=project).update(notify_level=NotifyLevel.all) + + user = f.UserFactory.create() + f.MembershipFactory.create(project=project, user=user, role=role) + + wiki_page = f.WikiPageFactory.create(project=project, owner=user, slug="test", content="test content") + + mail.outbox = [] + + url = reverse("wiki-links-list") + + data = { + "title": "test", + "href": "test", + "project": project.pk, + } + + assert project.wiki_pages.all().count() == 1 + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert len(mail.outbox) == 0 + assert project.wiki_pages.all().count() == 1 + + +def test_create_wiki_link_of_inexistent_wiki_page_with_permissions(client): + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_wiki_pages', 'view_wiki_link', + 'add_wiki_page', 'add_wiki_link']) + + f.MembershipFactory.create(project=project, user=project.owner, role=role) + project.owner.notify_policies.filter(project=project).update(notify_level=NotifyLevel.all) + + user = f.UserFactory.create() + f.MembershipFactory.create(project=project, user=user, role=role) + + mail.outbox = [] + + url = reverse("wiki-links-list") + + data = { + "title": "test", + "href": "test", + "project": project.pk, + } + + assert project.wiki_pages.all().count() == 0 + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert len(mail.outbox) == 1 + assert project.wiki_pages.all().count() == 1 + + +def test_create_wiki_link_of_inexistent_wiki_page_without_permissions(client): + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_wiki_pages', 'view_wiki_link', + 'add_wiki_link']) + + f.MembershipFactory.create(project=project, user=project.owner, role=role) + project.owner.notify_policies.filter(project=project).update(notify_level=NotifyLevel.all) + + user = f.UserFactory.create() + f.MembershipFactory.create(project=project, user=user, role=role) + + mail.outbox = [] + + url = reverse("wiki-links-list") + + data = { + "title": "test", + "href": "test", + "project": project.pk, + } + + assert project.wiki_pages.all().count() == 0 + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert len(mail.outbox) == 0 + assert project.wiki_pages.all().count() == 0 diff --git a/tests/models.py b/tests/models.py index 9583b8c0..e69de29b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,26 +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 -# 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 taiga.base import tags - - -class TaggedModel(tags.TaggedMixin, models.Model): - class Meta: - app_label = "tests" diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py index a8ce775f..1088550f 100644 --- a/tests/unit/test_export.py +++ b/tests/unit/test_export.py @@ -42,3 +42,17 @@ def test_export_user_story_finish_date(client): project_data = json.loads(output.getvalue()) finish_date = project_data["user_stories"][0]["finish_date"] assert finish_date == "2014-10-22T00:00:00+0000" + + +def test_export_epic_with_user_stories(client): + epic = f.EpicFactory.create(subject="test epic export") + user_story = f.UserStoryFactory.create(project=epic.project) + f.RelatedUserStory.create(epic=epic, user_story=user_story) + output = io.BytesIO() + render_project(user_story.project, output) + project_data = json.loads(output.getvalue()) + assert project_data["epics"][0]["subject"] == "test epic export" + assert len(project_data["epics"]) == 1 + + assert project_data["epics"][0]["related_user_stories"][0]["user_story"] == user_story.ref + assert len(project_data["epics"][0]["related_user_stories"]) == 1 diff --git a/tests/unit/test_import.py b/tests/unit/test_import.py new file mode 100644 index 00000000..58f9b9db --- /dev/null +++ b/tests/unit/test_import.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 +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import io +from .. import factories as f + +from taiga.base.utils import json +from taiga.export_import.services import render_project, store_project_from_dict + +pytestmark = pytest.mark.django_db + + +def test_import_epic_with_user_stories(client): + project = f.ProjectFactory() + project.default_points = f.PointsFactory.create(project=project) + project.default_issue_type = f.IssueTypeFactory.create(project=project) + project.default_issue_status = f.IssueStatusFactory.create(project=project) + project.default_epic_status = f.EpicStatusFactory.create(project=project) + project.default_us_status = f.UserStoryStatusFactory.create(project=project) + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.default_priority = f.PriorityFactory.create(project=project) + project.default_severity = f.SeverityFactory.create(project=project) + + epic = f.EpicFactory.create(subject="test epic export", project=project, status=project.default_epic_status) + user_story = f.UserStoryFactory.create(project=project, status=project.default_us_status, milestone=None) + f.RelatedUserStory.create(epic=epic, user_story=user_story, order=55) + output = io.BytesIO() + render_project(user_story.project, output) + project_data = json.loads(output.getvalue()) + + epic.project.delete() + + project = store_project_from_dict(project_data) + assert project.epics.count() == 1 + assert project.epics.first().ref == epic.ref + + assert project.epics.first().user_stories.count() == 1 + related_userstory = project.epics.first().relateduserstory_set.first() + assert related_userstory.user_story.ref == user_story.ref + assert related_userstory.order == 55 + assert related_userstory.epic.ref == epic.ref diff --git a/tests/unit/test_order_updates.py b/tests/unit/test_order_updates.py new file mode 100644 index 00000000..1a4a571c --- /dev/null +++ b/tests/unit/test_order_updates.py @@ -0,0 +1,163 @@ +# -*- 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.projects.services import apply_order_updates + + +def test_apply_order_updates_one_element_backward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "d": 2 + } + apply_order_updates(orders, new_orders) + assert orders == { + "d": 2, + "b": 3, + "c": 4 + } + + +def test_apply_order_updates_one_element_forward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "a": 3 + } + apply_order_updates(orders, new_orders) + assert orders == { + "a": 3, + "c": 4, + "d": 5, + "e": 6, + "f": 7 + } + + +def test_apply_order_updates_multiple_elements_backward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "d": 2, + "e": 3 + } + apply_order_updates(orders, new_orders) + assert orders == { + "d": 2, + "e": 3, + "b": 4, + "c": 5 + } + +def test_apply_order_updates_multiple_elements_forward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "a": 4, + "b": 5 + } + apply_order_updates(orders, new_orders) + assert orders == { + "a": 4, + "b": 5, + "d": 6, + "e": 7, + "f": 8 + } + +def test_apply_order_updates_two_elements(): + orders = { + "a": 0, + "b": 1, + } + new_orders = { + "b": 0 + } + apply_order_updates(orders, new_orders) + assert orders == { + "b": 0, + "a": 1 + } + +def test_apply_order_updates_duplicated_orders(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 3, + "e": 3, + "f": 4 + } + new_orders = { + "a": 3 + } + apply_order_updates(orders, new_orders) + assert orders == { + "a": 3, + "c": 4, + "d": 4, + "e": 4, + "f": 5 + } + +def test_apply_order_updates_multiple_elements_duplicated_orders(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 3, + "e": 3, + "f": 4 + } + new_orders = { + "c": 3, + "d": 3, + "a": 4 + } + apply_order_updates(orders, new_orders) + assert orders == { + "c": 3, + "d": 3, + "a": 4, + "e": 5, + "f": 6 + } diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index 26d7ea41..cc88552f 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -1,46 +1,61 @@ # -*- 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 . + import pytest -from .. import factories as f from django.db import models -from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin -from taiga.projects.models import Project +from taiga.base.api.validators import ModelValidator +from taiga.projects.validators import DuplicatedNameInProjectValidator pytestmark = pytest.mark.django_db(transaction=True) -import factory - class AuxProjectModel(models.Model): pass + class AuxModelWithNameAttribute(models.Model): name = models.CharField(max_length=255, null=False, blank=False) project = models.ForeignKey(AuxProjectModel, null=False, blank=False) -class AuxSerializer(ValidateDuplicatedNameInProjectMixin): +class AuxValidator(DuplicatedNameInProjectValidator, ModelValidator): class Meta: model = AuxModelWithNameAttribute - def test_duplicated_name_validation(): project = AuxProjectModel.objects.create() - instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project) + AuxModelWithNameAttribute.objects.create(name="1", project=project) instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project) # No duplicated_name - serializer = AuxSerializer(data={"name": "3", "project": project.id}) + validator = AuxValidator(data={"name": "3", "project": project.id}) - assert serializer.is_valid() + assert validator.is_valid() # Create duplicated_name - serializer = AuxSerializer(data={"name": "1", "project": project.id}) + validator = AuxValidator(data={"name": "1", "project": project.id}) - assert not serializer.is_valid() + assert not validator.is_valid() # Update name to existing one - serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) + validator = AuxValidator(data={"id": instance_2.id, "name": "1", "project": project.id}) - assert not serializer.is_valid() + assert not validator.is_valid() diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c564b094..2264e970 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -23,7 +23,7 @@ import django_sites as sites import re from taiga.base.utils.urls import get_absolute_url, is_absolute_url, build_url -from taiga.base.utils.db import save_in_bulk, update_in_bulk, update_in_bulk_with_ids, to_tsquery +from taiga.base.utils.db import save_in_bulk, update_in_bulk, to_tsquery pytestmark = pytest.mark.django_db @@ -82,21 +82,6 @@ def test_update_in_bulk_with_a_callback(): assert callback.call_count == 2 -def test_update_in_bulk_with_ids(): - ids = [1, 2] - new_values = [{"field1": 1}, {"field2": 2}] - model = mock.Mock() - - update_in_bulk_with_ids(ids, new_values, model) - - expected_calls = [ - mock.call(id=1), mock.call().update(field1=1), - mock.call(id=2), mock.call().update(field2=2) - ] - - model.objects.filter.assert_has_calls(expected_calls) - - TS_QUERY_TRANSFORMATIONS = [ ("1 OR 2", "1 | 2"), ("(1) 2", "( 1 ) & 2"),