diff --git a/AUTHORS.rst b/AUTHORS.rst index 70045203..ec62d162 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -20,12 +20,18 @@ answer newbie questions, and generally made taiga that much better: - Andrea Stagi - Andrés Moya - Andrey Alekseenko +<<<<<<< HEAD +======= +- Brett Profitt +>>>>>>> master - Bruno Clermont - Chris Wilson - David Burke - Hector Colina - Joe Letts - Julien Palard +- luyikei +- Motius GmbH - Ricky Posner - Yamila Moreno -- Brett Profitt +- Yaser Alraddadi diff --git a/CHANGELOG.md b/CHANGELOG.md index a570ed3d..f955a7d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog # +## 2.0.0 Pulsatilla Patens (2016-04-04) + +### Features +- Ability to create url custom fields. (thanks to [@astagi](https://github.com/astagi)). +- Blocked projects support +- Transfer projects ownership support +- Customizable max private and public projects per user +- Customizable max of memberships per owned private and public projects + +### Misc +- Lots of small and not so small bugfixes. + + ## 1.10.0 Dryas Octopetala (2016-01-30) ### Features @@ -10,7 +23,7 @@ - Filter projects list by - is_looking_for_people - is_featured - - is_backlog_activated + - is_backlog_activated - is_kanban_activated - Search projects by text query (order by ranking name > tags > description) - Order projects list: diff --git a/requirements-devel.txt b/requirements-devel.txt index da4f0eb9..e01aa38a 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,8 +1,8 @@ -r requirements.txt -factory_boy==2.6.0 +factory_boy==2.6.1 py==1.4.31 -pytest==2.8.5 +pytest==2.8.7 pytest-django==2.9.1 pytest-pythonpath==0.7 diff --git a/requirements.txt b/requirements.txt index 993c3787..181e863f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,37 +1,36 @@ -Django==1.8.6 +Django==1.9.2 #djangorestframework==2.3.13 # It's not necessary since Taiga 1.7 django-picklefield==0.3.2 -django-sampledatahelper==0.3.0 -gunicorn==19.3.0 +django-sampledatahelper==0.4.0 +gunicorn==19.4.5 psycopg2==2.6.1 -Pillow==2.9.0 +Pillow==3.1.1 pytz==2015.7 six==1.10.0 -amqp==1.4.7 -djmail==0.11 +amqp==1.4.9 +djmail==0.12.0.post1 django-pgjson==0.3.1 djorm-pgarray==1.2 -django-jinja==2.1.1 +django-jinja==2.1.2 jinja2==2.8 pygments==2.0.2 -django-sites==0.8 +django-sites==0.9 Markdown==2.6.5 fn==0.4.3 diff-match-patch==20121119 -requests==2.8.1 +requests==2.9.1 django-sr==0.0.4 -easy-thumbnails==2.2.1 -celery==3.1.19 +easy-thumbnails==2.3 +celery==3.1.20 redis==2.10.5 -Unidecode==0.04.18 -raven==5.9.2 +Unidecode==0.04.19 +raven==5.10.2 bleach==1.4.2 -django-ipware==1.1.2 -premailer==2.9.6 +django-ipware==1.1.3 +premailer==2.9.7 cssutils==1.0.1 # Compatible with python 3.5 -django-transactional-cleanup==0.1.15 lxml==3.5.0 git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea -pyjwkest==1.0.9 +pyjwkest==1.1.5 python-dateutil==2.4.2 netaddr==0.7.18 diff --git a/settings/common.py b/settings/common.py index 3687486b..568be60c 100644 --- a/settings/common.py +++ b/settings/common.py @@ -30,7 +30,7 @@ DEBUG = False DATABASES = { "default": { - "ENGINE": "transaction_hooks.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", "NAME": "taiga", } } @@ -320,7 +320,6 @@ INSTALLED_APPS = [ "sr", "easy_thumbnails", "raven.contrib.django.raven_compat", - "django_transactional_cleanup", ] WSGI_APPLICATION = "taiga.wsgi.application" @@ -347,7 +346,7 @@ LOGGING = { "handlers": { "null": { "level":"DEBUG", - "class":"django.utils.log.NullHandler", + "class":"logging.NullHandler", }, "console":{ "level":"DEBUG", @@ -434,7 +433,9 @@ REST_FRAMEWORK = { # Extra expose header related to Taiga APP (see taiga.base.middleware.cors=) APP_EXTRA_EXPOSE_HEADERS = [ "taiga-info-total-opened-milestones", - "taiga-info-total-closed-milestones" + "taiga-info-total-closed-milestones", + "taiga-info-project-memberships", + "taiga-info-project-is-private" ] DEFAULT_PROJECT_TEMPLATE = "scrum" @@ -522,6 +523,12 @@ WEBHOOKS_ENABLED = False FRONT_SITEMAP_ENABLED = False FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second +EXTRA_BLOCKING_CODES = [] + +MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit +MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit +MAX_MEMBERSHIPS_PRIVATE_PROJECTS = None # None == no limit +MAX_MEMBERSHIPS_PUBLIC_PROJECTS = None # None == no limit from .sr import * diff --git a/settings/local.py.example b/settings/local.py.example index 28f5abb9..e1bd9383 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -1,6 +1,7 @@ # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -24,7 +25,7 @@ from .development import * DATABASES = { 'default': { - 'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'taiga', 'USER': 'taiga', 'PASSWORD': 'changeme', diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 49f9ceaa..5015a02e 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -25,6 +25,7 @@ not uses clasess and uses simple functions. """ from django.apps import apps +from django.contrib.auth import get_user_model from django.db import transaction as tx from django.db import IntegrityError from django.utils.translation import ugettext as _ @@ -69,7 +70,7 @@ def is_user_already_registered(*, username:str, email:str) -> (bool, str): and in case he does whats the duplicated attribute """ - user_model = apps.get_model("users", "User") + user_model = get_user_model() if user_model.objects.filter(username=username): return (True, _("Username is already in use.")) @@ -110,7 +111,7 @@ def public_register(username:str, password:str, email:str, full_name:str): if is_registered: raise exc.WrongArguments(reason) - user_model = apps.get_model("users", "User") + user_model = get_user_model() user = user_model(username=username, email=email, full_name=full_name) @@ -159,7 +160,7 @@ def private_register_for_new_user(token:str, username:str, email:str, if is_registered: raise exc.WrongArguments(reason) - user_model = apps.get_model("users", "User") + user_model = get_user_model() user = user_model(username=username, email=email, full_name=full_name) diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py index 4d809e38..58fe2938 100644 --- a/taiga/auth/tokens.py +++ b/taiga/auth/tokens.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - +from django.contrib.auth import get_user_model from taiga.base import exceptions as exc from django.apps import apps @@ -47,7 +47,7 @@ def get_user_for_token(token, scope, max_age=None): except signing.BadSignature: raise exc.NotAuthenticated(_("Invalid token")) - model_cls = apps.get_model("users", "User") + model_cls = get_user_model() try: user = model_cls.objects.get(pk=data["user_%s_id" % (scope)]) diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index 50fe58f1..365e4070 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -64,17 +64,17 @@ from django.utils.encoding import is_protected_type from django.utils.functional import Promise from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ -from django.utils.datastructures import SortedDict from . import ISO_8601 from .settings import api_settings +from collections import OrderedDict +from decimal import Decimal, DecimalException import copy import datetime import inspect import re import warnings -from decimal import Decimal, DecimalException def is_non_str_iterable(obj): @@ -255,7 +255,7 @@ class Field(object): return [self.to_native(item) for item in value] elif isinstance(value, dict): # Make sure we preserve field ordering, if it exists - ret = SortedDict() + ret = OrderedDict() for key, val in value.items(): ret[key] = self.to_native(val) return ret @@ -270,7 +270,7 @@ class Field(object): return {} def metadata(self): - metadata = SortedDict() + metadata = OrderedDict() metadata["type"] = self.type_label metadata["required"] = getattr(self, "required", False) optional_attrs = ["read_only", "label", "help_text", diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index c558f750..1125fcd9 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -53,9 +53,9 @@ from taiga.base import response from .settings import api_settings from .utils import get_object_or_404 +from .. import exceptions as exc from ..decorators import model_pk_lock - def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): """ Given a model instance, and an optional pk and slug field, @@ -243,3 +243,32 @@ class DestroyModelMixin: obj.delete() self.post_delete(obj) return response.NoContent() + + +class BlockeableModelMixin: + def is_blocked(self, obj): + raise NotImplementedError("is_blocked must be overridden") + + def pre_conditions_blocked(self, obj): + #Raises permission exception + if obj is not None and self.is_blocked(obj): + raise exc.Blocked(_("Blocked element")) + + +class BlockeableSaveMixin(BlockeableModelMixin): + def pre_conditions_on_save(self, obj): + # Called on create and update calls + self.pre_conditions_blocked(obj) + super().pre_conditions_on_save(obj) + + +class BlockeableDeleteMixin(): + def pre_conditions_on_delete(self, obj): + # Called on destroy call + self.pre_conditions_blocked(obj) + super().pre_conditions_on_delete(obj) + + +class BlockedByProjectMixin(BlockeableSaveMixin, BlockeableDeleteMixin): + def is_blocked(self, obj): + return obj.project is not None and obj.project.blocked_code is not None diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py index b43d0067..62b40619 100644 --- a/taiga/base/api/permissions.py +++ b/taiga/base/api/permissions.py @@ -20,7 +20,7 @@ import abc from functools import reduce from taiga.base.utils import sequence as sq -from taiga.permissions.service import user_has_perm, is_project_owner +from taiga.permissions.service import user_has_perm, is_project_admin from django.apps import apps from django.utils.translation import ugettext as _ @@ -206,9 +206,9 @@ class HasMandatoryParam(PermissionComponent): return False -class IsProjectOwner(PermissionComponent): +class IsProjectAdmin(PermissionComponent): def check_permissions(self, request, view, obj=None): - return is_project_owner(request.user, obj) + return is_project_admin(request.user, obj) class IsObjectOwner(PermissionComponent): diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 8216ddf4..55ae824f 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -59,11 +59,11 @@ from django.core.paginator import Page from django.db import models from django.forms import widgets from django.utils import six -from django.utils.datastructures import SortedDict from django.utils.translation import ugettext as _ from .settings import api_settings +from collections import OrderedDict import copy import datetime import inspect @@ -148,7 +148,7 @@ class DictWithMetadata(dict): return dict(self) -class SortedDictWithMetadata(SortedDict): +class OrderedDictWithMetadata(OrderedDict): """ A sorted dict-like object, that can have additional properties attached. """ @@ -158,7 +158,7 @@ class SortedDictWithMetadata(SortedDict): Overriden to remove the metadata from the dict, since it shouldn't be pickle and may in some instances be unpickleable. """ - return SortedDict(self).__dict__ + return OrderedDict(self).__dict__ def _is_protected_type(obj): @@ -194,7 +194,7 @@ def _get_declared_fields(bases, attrs): if hasattr(base, "base_fields"): fields = list(base.base_fields.items()) + fields - return SortedDict(fields) + return OrderedDict(fields) class SerializerMetaclass(type): @@ -222,7 +222,7 @@ class BaseSerializer(WritableField): pass _options_class = SerializerOptions - _dict_class = SortedDictWithMetadata + _dict_class = OrderedDictWithMetadata def __init__(self, instance=None, data=None, files=None, context=None, partial=False, many=None, @@ -268,7 +268,7 @@ class BaseSerializer(WritableField): This will be the set of any explicitly declared fields, plus the set of fields returned by get_default_fields(). """ - ret = SortedDict() + ret = OrderedDict() # Get the explicitly declared fields base_fields = copy.deepcopy(self.base_fields) @@ -284,7 +284,7 @@ class BaseSerializer(WritableField): # If "fields" is specified, use those fields, in that order. if self.opts.fields: assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple" - new = SortedDict() + new = OrderedDict() for key in self.opts.fields: new[key] = ret[key] ret = new @@ -458,7 +458,10 @@ class BaseSerializer(WritableField): many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type)) if many: - return [self.to_native(item) for item in value] + try: + return [self.to_native(item) for item in value] + except TypeError: + pass # LazyObject is iterable so we need to catch this return self.to_native(value) def field_from_native(self, data, files, field_name, into): @@ -610,7 +613,10 @@ class BaseSerializer(WritableField): DeprecationWarning, stacklevel=2) if many: - self._data = [self.to_native(item) for item in obj] + try: + self._data = [self.to_native(item) for item in obj] + except TypeError: + self._data = self.to_native(obj) # LazyObject is iterable so we need to catch this else: self._data = self.to_native(obj) @@ -645,7 +651,7 @@ class BaseSerializer(WritableField): Useful for things like responding to OPTIONS requests, or generating API schemas for auto-documentation. """ - return SortedDict( + return OrderedDict( [(field_name, field.metadata()) for field_name, field in six.iteritems(self.fields)] ) @@ -740,7 +746,7 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer))) assert cls is not None, \ "Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__ opts = cls._meta.concrete_model._meta - ret = SortedDict() + ret = OrderedDict() nested = bool(self.opts.depth) # Deal with adding the primary key field diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py index bada39b7..9b894be6 100644 --- a/taiga/base/api/settings.py +++ b/taiga/base/api/settings.py @@ -62,9 +62,10 @@ back to the defaults. from __future__ import unicode_literals from django.conf import settings -from django.utils import importlib from django.utils import six +import importlib + from . import ISO_8601 diff --git a/taiga/base/api/templates/api/base.html b/taiga/base/api/templates/api/base.html index 074b1e9e..26921db5 100644 --- a/taiga/base/api/templates/api/base.html +++ b/taiga/base/api/templates/api/base.html @@ -1,4 +1,3 @@ -{% load url from future %} {% load api %} diff --git a/taiga/base/api/templates/api/login_base.html b/taiga/base/api/templates/api/login_base.html index 118fdbc5..96870344 100644 --- a/taiga/base/api/templates/api/login_base.html +++ b/taiga/base/api/templates/api/login_base.html @@ -1,4 +1,3 @@ -{% load url from future %} {% load api %} diff --git a/taiga/base/api/utils/encoders.py b/taiga/base/api/utils/encoders.py index cf792697..0f878914 100644 --- a/taiga/base/api/utils/encoders.py +++ b/taiga/base/api/utils/encoders.py @@ -45,13 +45,10 @@ Helper classes for parsers. """ from django.db.models.query import QuerySet -from django.utils.datastructures import SortedDict from django.utils.functional import Promise from django.utils import timezone from django.utils.encoding import force_text -from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata - import datetime import decimal import types diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py index 68f58e27..791523d4 100644 --- a/taiga/base/api/views.py +++ b/taiga/base/api/views.py @@ -43,6 +43,8 @@ import json +from collections import OrderedDict + from django.conf import settings from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponse @@ -50,7 +52,6 @@ from django.http.response import HttpResponseBase from django.views.decorators.csrf import csrf_exempt from django.views.defaults import server_error from django.views.generic import View -from django.utils.datastructures import SortedDict from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ @@ -462,7 +463,7 @@ class APIView(View): # By default we can't provide any form-like information, however the # generic views override this implementation and add additional # information for POST and PUT methods, based on the serializer. - ret = SortedDict() + ret = OrderedDict() ret['name'] = self.get_view_name() ret['description'] = self.get_view_description() ret['renders'] = [renderer.media_type for renderer in self.renderer_classes] diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py index 9c5f1e5d..af2d2789 100644 --- a/taiga/base/api/viewsets.py +++ b/taiga/base/api/viewsets.py @@ -187,11 +187,13 @@ class ModelListViewSet(mixins.RetrieveModelMixin, GenericViewSet): pass + class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin, mixins.RetrieveModelMixin, GenericViewSet): pass + class ModelRetrieveViewSet(mixins.RetrieveModelMixin, GenericViewSet): pass diff --git a/taiga/base/apps.py b/taiga/base/apps.py index 5a35fa30..b56aaafb 100644 --- a/taiga/base/apps.py +++ b/taiga/base/apps.py @@ -17,12 +17,14 @@ from django.apps import AppConfig -from .signals.thumbnails import connect_thumbnail_signals - class BaseAppConfig(AppConfig): name = "taiga.base" verbose_name = "Base App Config" def ready(self): + from .signals.thumbnails import connect_thumbnail_signals + from .signals.cleanup_files import connect_cleanup_files_signals + connect_thumbnail_signals() + connect_cleanup_files_signals() diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py index 0ffe8e10..afddb058 100644 --- a/taiga/base/decorators.py +++ b/taiga/base/decorators.py @@ -17,7 +17,6 @@ from django_pglocks import advisory_lock - def detail_route(methods=['get'], **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index 3e7c2104..104ba896 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -201,6 +201,28 @@ class NotAuthenticated(NotAuthenticated): pass +class Blocked(APIException): + """ + Exception used on blocked projects + """ + status_code = status.HTTP_451_BLOCKED + default_detail = _("Blocked element") + + +class NotEnoughSlotsForProject(BaseException): + """ + Exception used on import/edition/creation project errors where the user + hasn't slots enough + """ + default_detail = _("No room left for more projects.") + + def __init__(self, is_private, total_memberships, detail=None): + self.detail = detail or self.default_detail + self.project_data = { + "is_private": is_private, + "total_memberships": total_memberships + } + def format_exception(exc): if isinstance(exc.detail, (dict, list, tuple,)): detail = exc.detail @@ -232,6 +254,9 @@ def exception_handler(exc): headers["WWW-Authenticate"] = exc.auth_header if getattr(exc, "wait", None): headers["X-Throttle-Wait-Seconds"] = "%d" % exc.wait + if getattr(exc, "project_data", None): + headers["Taiga-Info-Project-Memberships"] = exc.project_data["total_memberships"] + headers["Taiga-Info-Project-Is-Private"] = exc.project_data["is_private"] detail = format_exception(exc) return response.Response(detail, status=exc.status_code, headers=headers) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index a2932551..ea962af0 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + import logging from django.apps import apps @@ -141,7 +142,7 @@ class PermissionBasedFilterBackend(FilterBackend): if project_id: memberships_qs = memberships_qs.filter(project_id=project_id) memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) | - Q(is_owner=True)) + Q(is_admin=True)) projects_list = [membership.project_id for membership in memberships_qs] @@ -242,7 +243,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend): if project_id: memberships_qs = memberships_qs.filter(project_id=project_id) memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) | - Q(is_owner=True)) + Q(is_admin=True)) projects_list = [membership.project_id for membership in memberships_qs] @@ -286,7 +287,7 @@ class BaseIsProjectAdminFilterBackend(object): return [] membership_model = apps.get_model('projects', 'Membership') - memberships_qs = membership_model.objects.filter(user=request.user, is_owner=True) + memberships_qs = membership_model.objects.filter(user=request.user, is_admin=True) if project_id: memberships_qs = memberships_qs.filter(project_id=project_id) diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py index c0f1a490..53a63a87 100644 --- a/taiga/base/management/commands/test_emails.py +++ b/taiga/base/management/commands/test_emails.py @@ -19,7 +19,8 @@ import datetime from optparse import make_option -from django.db.models.loading import get_model +from django.apps import apps +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand from django.utils import timezone @@ -28,7 +29,6 @@ from taiga.base.mails import mail_builder from taiga.projects.models import Project, Membership from taiga.projects.history.models import HistoryEntry from taiga.projects.history.services import get_history_queryset_by_model_instance -from taiga.users.models import User class Command(BaseCommand): @@ -50,7 +50,7 @@ class Command(BaseCommand): # Register email context = {"lang": locale, - "user": User.objects.all().order_by("?").first(), + "user": get_user_model().objects.all().order_by("?").first(), "cancel_token": "cancel-token"} email = mail_builder.registered_user(test_email, context) @@ -58,7 +58,7 @@ class Command(BaseCommand): # Membership invitation membership = Membership.objects.order_by("?").filter(user__isnull=True).first() - membership.invited_by = User.objects.all().order_by("?").first() + membership.invited_by = get_user_model().objects.all().order_by("?").first() membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example" context = {"lang": locale, "membership": membership} @@ -88,19 +88,19 @@ class Command(BaseCommand): email.send() # Password recovery - context = {"lang": locale, "user": User.objects.all().order_by("?").first()} + context = {"lang": locale, "user": get_user_model().objects.all().order_by("?").first()} email = mail_builder.password_recovery(test_email, context) email.send() # Change email - context = {"lang": locale, "user": User.objects.all().order_by("?").first()} + context = {"lang": locale, "user": get_user_model().objects.all().order_by("?").first()} email = mail_builder.change_email(test_email, context) email.send() # Export/Import emails context = { "lang": locale, - "user": User.objects.all().order_by("?").first(), + "user": get_user_model().objects.all().order_by("?").first(), "project": Project.objects.all().order_by("?").first(), "error_subject": "Error generating project dump", "error_message": "Error generating project dump", @@ -109,7 +109,7 @@ class Command(BaseCommand): email.send() context = { "lang": locale, - "user": User.objects.all().order_by("?").first(), + "user": get_user_model().objects.all().order_by("?").first(), "error_subject": "Error importing project dump", "error_message": "Error importing project dump", } @@ -120,7 +120,7 @@ class Command(BaseCommand): context = { "lang": locale, "url": "http://dummyurl.com", - "user": User.objects.all().order_by("?").first(), + "user": get_user_model().objects.all().order_by("?").first(), "project": Project.objects.all().order_by("?").first(), "deletion_date": deletion_date, } @@ -129,7 +129,7 @@ class Command(BaseCommand): context = { "lang": locale, - "user": User.objects.all().order_by("?").first(), + "user": get_user_model().objects.all().order_by("?").first(), "project": Project.objects.all().order_by("?").first(), } email = mail_builder.load_dump(test_email, context) @@ -157,13 +157,13 @@ class Command(BaseCommand): context = { "lang": locale, "project": Project.objects.all().order_by("?").first(), - "changer": User.objects.all().order_by("?").first(), + "changer": get_user_model().objects.all().order_by("?").first(), "history_entries": HistoryEntry.objects.all().order_by("?")[0:5], - "user": User.objects.all().order_by("?").first(), + "user": get_user_model().objects.all().order_by("?").first(), } for notification_email in notification_emails: - model = get_model(*notification_email[0].split(".")) + model = apps.get_model(*notification_email[0].split(".")) snapshot = { "subject": "Tests subject", "ref": 123123, @@ -187,3 +187,38 @@ class Command(BaseCommand): cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]}) email = cls() email.send(test_email, context) + + + # Transfer Emails + context = { + "project": Project.objects.all().order_by("?").first(), + "requester": User.objects.all().order_by("?").first(), + } + email = mail_builder.transfer_request(test_email, context) + email.send() + + context = { + "project": Project.objects.all().order_by("?").first(), + "receiver": User.objects.all().order_by("?").first(), + "token": "test-token", + "reason": "Test reason" + } + email = mail_builder.transfer_start(test_email, context) + email.send() + + context = { + "project": Project.objects.all().order_by("?").first(), + "old_owner": User.objects.all().order_by("?").first(), + "new_owner": User.objects.all().order_by("?").first(), + "reason": "Test reason" + } + email = mail_builder.transfer_accept(test_email, context) + email.send() + + context = { + "project": Project.objects.all().order_by("?").first(), + "rejecter": User.objects.all().order_by("?").first(), + "reason": "Test reason" + } + email = mail_builder.transfer_reject(test_email, context) + email.send() diff --git a/taiga/base/response.py b/taiga/base/response.py index 3698ca7a..5b84123a 100644 --- a/taiga/base/response.py +++ b/taiga/base/response.py @@ -43,9 +43,10 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """The various HTTP responses for use in returning proper HTTP codes.""" +from http.client import responses + from django import http -from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse from django.utils import six @@ -114,7 +115,7 @@ class Response(SimpleTemplateResponse): """ # TODO: Deprecate and use a template tag instead # TODO: Status code text for RFC 6585 status codes - return STATUS_CODE_TEXT.get(self.status_code, '') + return responses.get(self.status_code, '') def __getstate__(self): """ diff --git a/taiga/base/signals/cleanup_files.py b/taiga/base/signals/cleanup_files.py new file mode 100644 index 00000000..0efab210 --- /dev/null +++ b/taiga/base/signals/cleanup_files.py @@ -0,0 +1,97 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.apps import apps +from django.db import models, connection +from django.db.utils import DEFAULT_DB_ALIAS, ConnectionHandler +from django.db.models.signals import pre_save, post_delete + +import logging +logger = logging.getLogger(__name__) + + +from django.dispatch import Signal + +cleanup_pre_delete = Signal(providing_args=["file"]) +cleanup_post_delete = Signal(providing_args=["file"]) + + + +def _find_models_with_filefield(): + result = [] + for model in apps.get_models(): + for field in model._meta.fields: + if isinstance(field, models.FileField): + result.append(model) + break + return result + + +def _delete_file(file_obj): + def delete_from_storage(): + try: + cleanup_pre_delete.send(sender=None, file=file_obj) + storage.delete(file_obj.name) + cleanup_post_delete.send(sender=None, file=file_obj) + except Exception: + logger.exception("Unexpected exception while attempting " + "to delete old file '%s'".format(file_obj.name)) + + storage = file_obj.storage + if storage and storage.exists(file_obj.name): + connection.on_commit(delete_from_storage) + + +def _get_file_fields(instance): + return filter( + lambda field: isinstance(field, models.FileField), + instance._meta.fields, + ) + + +def remove_files_on_change(sender, instance, **kwargs): + if not instance.pk: + return + + try: + old_instance = sender.objects.get(pk=instance.pk) + except instance.DoesNotExist: + return + + for field in _get_file_fields(instance): + old_file = getattr(old_instance, field.name) + new_file = getattr(instance, field.name) + + if old_file and old_file != new_file: + _delete_file(old_file) + + +def remove_files_on_delete(sender, instance, **kwargs): + for field in _get_file_fields(instance): + file_to_delete = getattr(instance, field.name) + + if file_to_delete: + _delete_file(file_to_delete) + + +def connect_cleanup_files_signals(): + connections = ConnectionHandler() + backend = connections[DEFAULT_DB_ALIAS] + + for model in _find_models_with_filefield(): + pre_save.connect(remove_files_on_change, sender=model) + post_delete.connect(remove_files_on_delete, sender=model) diff --git a/taiga/base/signals/thumbnails.py b/taiga/base/signals/thumbnails.py index 5709a59b..53bc1bca 100644 --- a/taiga/base/signals/thumbnails.py +++ b/taiga/base/signals/thumbnails.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django_transactional_cleanup.signals import cleanup_post_delete +from .cleanup_files import cleanup_post_delete from easy_thumbnails.files import get_thumbnailer diff --git a/taiga/base/status.py b/taiga/base/status.py index 08386721..003c771b 100644 --- a/taiga/base/status.py +++ b/taiga/base/status.py @@ -104,6 +104,7 @@ HTTP_417_EXPECTATION_FAILED = 417 HTTP_428_PRECONDITION_REQUIRED = 428 HTTP_429_TOO_MANY_REQUESTS = 429 HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 +HTTP_451_BLOCKED = 451 HTTP_500_INTERNAL_SERVER_ERROR = 500 HTTP_501_NOT_IMPLEMENTED = 501 HTTP_502_BAD_GATEWAY = 502 diff --git a/taiga/contrib_routers.py b/taiga/contrib_routers.py deleted file mode 100644 index 85b869dc..00000000 --- a/taiga/contrib_routers.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from taiga.base import routers - -router = routers.DefaultRouter(trailing_slash=False) diff --git a/taiga/events/apps.py b/taiga/events/apps.py index 13da3d83..6b4c6a59 100644 --- a/taiga/events/apps.py +++ b/taiga/events/apps.py @@ -19,15 +19,16 @@ import sys from django.apps import AppConfig from django.db.models import signals -from . import signal_handlers as handlers def connect_events_signals(): + from . import signal_handlers as handlers signals.post_save.connect(handlers.on_save_any_model, dispatch_uid="events_change") signals.post_delete.connect(handlers.on_delete_any_model, dispatch_uid="events_delete") def disconnect_events_signals(): + from . import signal_handlers as handlers signals.post_save.disconnect(dispatch_uid="events_change") signals.post_delete.disconnect(dispatch_uid="events_delete") diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 19ccd373..1ae5ca4b 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -36,6 +36,7 @@ from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.serializers import ProjectSerializer +from taiga.users import services as users_service from . import mixins from . import serializers @@ -90,6 +91,14 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi data = request.DATA.copy() data['owner'] = data.get('owner', request.user.email) + is_private = data.get('is_private', False) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + self.request.user, + Project(is_private=is_private, id=None) + ) + if not enough_slots: + raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error) + # Create Project project_serialized = service.store_project(data) @@ -106,11 +115,19 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Create memberships if "memberships" in data: + members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]]) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + self.request.user, + Project(is_private=is_private, id=None), + members + ) + if not enough_slots: + raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error) service.store_memberships(project_serialized.object, data) try: owner_membership = project_serialized.object.memberships.get(user=project_serialized.object.owner) - owner_membership.is_owner = True + owner_membership.is_admin = True owner_membership.save() except Membership.DoesNotExist: Membership.objects.create( @@ -118,7 +135,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi email=project_serialized.object.owner.email, user=project_serialized.object.owner, role=project_serialized.object.roles.all().first(), - is_owner=True + is_admin=True ) # Create project values choicess @@ -202,6 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi try: dump = json.load(reader(dump)) + is_private = dump.get("is_private", False) except Exception: raise exc.WrongArguments(_("Invalid dump format")) @@ -209,11 +227,23 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if slug is not None and Project.objects.filter(slug=slug).exists(): del dump['slug'] + user = request.user + dump['owner'] = user.email + + members = len([m for m in dump.get("memberships", []) if m.get("email", None) != dump["owner"]]) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + user, + Project(is_private=is_private, id=None), + members + ) + if not enough_slots: + raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error) + if settings.CELERY_ENABLED: - task = tasks.load_project_dump.delay(request.user, dump) + task = tasks.load_project_dump.delay(user, dump) return response.Accepted({"import_id": task.id}) - project = dump_service.dict_to_project(dump, request.user.email) + project = dump_service.dict_to_project(dump, request.user) response_data = ProjectSerializer(project).data return response.Created(response_data) diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 8029fa0f..b68f3bf9 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -17,7 +17,8 @@ from django.utils.translation import ugettext as _ -from taiga.projects.models import Membership +from taiga.projects.models import Membership, Project +from taiga.users import services as users_service from . import serializers from . import service @@ -89,7 +90,15 @@ def store_tags_colors(project, data): def dict_to_project(data, owner=None): if owner: - data["owner"] = owner + data["owner"] = owner.email + members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]]) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + owner, + Project(is_private=data.get("is_private", False), id=None), + members + ) + if not enough_slots: + raise TaigaImportError(not_enough_slots_error) project_serialized = service.store_project(data) @@ -138,7 +147,7 @@ def dict_to_project(data, owner=None): email=proj.owner.email, user=proj.owner, role=proj.roles.all().first(), - is_owner=True + is_admin=True ) if service.get_errors(clear=False): diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index 1b44adbf..367a2401 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -25,6 +25,7 @@ from taiga.projects.models import Project from taiga.export_import.renderers import ExportRenderer from taiga.export_import.dump_service import dict_to_project, TaigaImportError from taiga.export_import.service import get_errors +from taiga.users.models import User class Command(BaseCommand): @@ -58,7 +59,9 @@ class Command(BaseCommand): except Project.DoesNotExist: pass signals.post_delete.receivers = receivers_back - dict_to_project(data, args[1]) + + user = User.objects.get(email=args[1]) + dict_to_project(data, user) except TaigaImportError as e: print("ERROR:", end=" ") print(e.message) diff --git a/taiga/export_import/permissions.py b/taiga/export_import/permissions.py index 1e0a1dec..22a03ebe 100644 --- a/taiga/export_import/permissions.py +++ b/taiga/export_import/permissions.py @@ -17,11 +17,11 @@ from taiga.base.api.permissions import (TaigaResourcePermission, - IsProjectOwner, IsAuthenticated) + IsProjectAdmin, IsAuthenticated) class ImportExportPermission(TaigaResourcePermission): import_project_perms = IsAuthenticated() - import_item_perms = IsProjectOwner() - export_project_perms = IsProjectOwner() + import_item_perms = IsProjectAdmin() + export_project_perms = IsProjectAdmin() load_dump_perms = IsAuthenticated() diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 74df0cbc..55b2031d 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -21,6 +21,7 @@ import os from collections import OrderedDict from django.apps import apps +from django.contrib.auth import get_user_model from django.core.files.base import ContentFile from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError @@ -29,10 +30,10 @@ from django.utils.translation import ugettext as _ from django.contrib.contenttypes.models import ContentType -from taiga import mdrender from taiga.base.api import serializers from taiga.base.fields import JsonField, PgArrayField +from taiga.mdrender.service import render as mdrender from taiga.projects import models as projects_models from taiga.projects.custom_attributes import models as custom_attributes_models from taiga.projects.userstories import models as userstories_models @@ -154,7 +155,7 @@ class CommentField(serializers.WritableField): def field_from_native(self, data, files, field_name, into): super().field_from_native(data, files, field_name, into) - into["comment_html"] = mdrender.render(self.context['project'], data.get("comment", "")) + into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) class ProjectRelatedField(serializers.RelatedField): @@ -263,7 +264,7 @@ class WatcheableObjectModelSerializer(serializers.ModelSerializer): adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) - User = apps.get_model("users", "User") + User = get_user_model() adding_users = User.objects.filter(email__in=adding_watcher_emails) removing_users = User.objects.filter(email__in=removing_watcher_emails) diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index c6389b8b..8044f35c 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -79,7 +79,7 @@ def delete_project_dump(project_id, project_slug, task_id): @app.task def load_project_dump(user, dump): try: - project = dict_to_project(dump, user.email) + project = dict_to_project(dump, user) except Exception: ctx = { "user": user, diff --git a/taiga/export_import/templates/emails/dump_project-body-html.jinja b/taiga/export_import/templates/emails/dump_project-body-html.jinja index 86906ded..18b234c0 100644 --- a/taiga/export_import/templates/emails/dump_project-body-html.jinja +++ b/taiga/export_import/templates/emails/dump_project-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, project=project.name|safe, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %} + {% trans user=user.get_full_name(), project=project.name, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %}

Project dump generated

Hello {{ user }},

Your dump from project {{ project }} has been correctly generated.

diff --git a/taiga/export_import/templates/emails/dump_project-body-text.jinja b/taiga/export_import/templates/emails/dump_project-body-text.jinja index 810d373d..6e12008b 100644 --- a/taiga/export_import/templates/emails/dump_project-body-text.jinja +++ b/taiga/export_import/templates/emails/dump_project-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, project=project.name|safe, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %} +{% trans user=user.get_full_name(), project=project.name, url=url, deletion_date=deletion_date|date("SHORT_DATETIME_FORMAT") + deletion_date|date(" T") %} Hello {{ user }}, Your dump from project {{ project }} has been correctly generated. You can download it here: diff --git a/taiga/export_import/templates/emails/dump_project-subject.jinja b/taiga/export_import/templates/emails/dump_project-subject.jinja index 7ad0ef61..f723a922 100644 --- a/taiga/export_import/templates/emails/dump_project-subject.jinja +++ b/taiga/export_import/templates/emails/dump_project-subject.jinja @@ -1 +1 @@ -{% trans project=project.name|safe %}[{{ project }}] Your project dump has been generated{% endtrans %} +{% trans project=project.name %}[{{ project }}] Your project dump has been generated{% endtrans %} diff --git a/taiga/export_import/templates/emails/export_error-body-html.jinja b/taiga/export_import/templates/emails/export_error-body-html.jinja index 12ab5b10..0ae593e9 100644 --- a/taiga/export_import/templates/emails/export_error-body-html.jinja +++ b/taiga/export_import/templates/emails/export_error-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email"), project=project.name|safe %} + {% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email"), project=project.name %}

{{ error_message }}

Hello {{ user }},

Your project {{ project }} has not been exported correctly.

diff --git a/taiga/export_import/templates/emails/export_error-body-text.jinja b/taiga/export_import/templates/emails/export_error-body-text.jinja index e9efcb16..2eab60a5 100644 --- a/taiga/export_import/templates/emails/export_error-body-text.jinja +++ b/taiga/export_import/templates/emails/export_error-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email"), project=project.name|safe %} +{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email"), project=project.name %} Hello {{ user }}, {{ error_message }} diff --git a/taiga/export_import/templates/emails/export_error-subject.jinja b/taiga/export_import/templates/emails/export_error-subject.jinja index fed22ace..e5d020cf 100644 --- a/taiga/export_import/templates/emails/export_error-subject.jinja +++ b/taiga/export_import/templates/emails/export_error-subject.jinja @@ -1 +1 @@ -{% trans error_subject=error_subject, project=project.name|safe %}[{{ project }}] {{ error_subject }}{% endtrans %} +{% trans error_subject=error_subject, project=project.name %}[{{ project }}] {{ error_subject }}{% endtrans %} diff --git a/taiga/export_import/templates/emails/import_error-body-html.jinja b/taiga/export_import/templates/emails/import_error-body-html.jinja index b1a27a72..52afe388 100644 --- a/taiga/export_import/templates/emails/import_error-body-html.jinja +++ b/taiga/export_import/templates/emails/import_error-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email") %} + {% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email") %}

{{ error_message }}

Hello {{ user }},

Your project has not been importer correctly.

diff --git a/taiga/export_import/templates/emails/import_error-body-text.jinja b/taiga/export_import/templates/emails/import_error-body-text.jinja index 50f1ccb4..7a9d782e 100644 --- a/taiga/export_import/templates/emails/import_error-body-text.jinja +++ b/taiga/export_import/templates/emails/import_error-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, error_message=error_message, support_email=sr("support.email") %} +{% trans user=user.get_full_name(), error_message=error_message, support_email=sr("support.email") %} Hello {{ user }}, {{ error_message }} diff --git a/taiga/export_import/templates/emails/load_dump-body-html.jinja b/taiga/export_import/templates/emails/load_dump-body-html.jinja index 52d7fd7f..564ccff3 100644 --- a/taiga/export_import/templates/emails/load_dump-body-html.jinja +++ b/taiga/export_import/templates/emails/load_dump-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, url=resolve_front_url("project", project.slug), project=project.name|safe %} + {% trans user=user.get_full_name(), url=resolve_front_url("project", project.slug), project=project.name %}

Project dump imported

Hello {{ user }},

Your project dump has been correctly imported.

diff --git a/taiga/export_import/templates/emails/load_dump-body-text.jinja b/taiga/export_import/templates/emails/load_dump-body-text.jinja index e5f0aa24..3cb40ad2 100644 --- a/taiga/export_import/templates/emails/load_dump-body-text.jinja +++ b/taiga/export_import/templates/emails/load_dump-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, url=resolve_front_url("project", project.slug), project=project.name|safe %} +{% trans user=user.get_full_name(), url=resolve_front_url("project", project.slug), project=project.name %} Hello {{ user }}, Your project dump has been correctly imported. diff --git a/taiga/export_import/templates/emails/load_dump-subject.jinja b/taiga/export_import/templates/emails/load_dump-subject.jinja index a258d42e..6ef621c4 100644 --- a/taiga/export_import/templates/emails/load_dump-subject.jinja +++ b/taiga/export_import/templates/emails/load_dump-subject.jinja @@ -1 +1 @@ -{% trans project=project.name|safe %}[{{ project }}] Your project dump has been imported{% endtrans %} +{% trans project=project.name %}[{{ project }}] Your project dump has been imported{% endtrans %} diff --git a/taiga/feedback/apps.py b/taiga/feedback/apps.py index 505b924b..b48a3cfb 100644 --- a/taiga/feedback/apps.py +++ b/taiga/feedback/apps.py @@ -20,8 +20,6 @@ from django.apps import apps from django.conf import settings from django.conf.urls import include, url -from .routers import router - class FeedbackAppConfig(AppConfig): name = "taiga.feedback" @@ -30,4 +28,5 @@ class FeedbackAppConfig(AppConfig): def ready(self): if settings.FEEDBACK_ENABLED: from taiga.urls import urlpatterns + from .routers import router urlpatterns.append(url(r'^api/v1/', include(router.urls))) diff --git a/taiga/feedback/templates/emails/feedback_notification-body-html.jinja b/taiga/feedback/templates/emails/feedback_notification-body-html.jinja index b2c38449..3417e7c2 100644 --- a/taiga/feedback/templates/emails/feedback_notification-body-html.jinja +++ b/taiga/feedback/templates/emails/feedback_notification-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %} + {% trans full_name=feedback_entry.full_name, email=feedback_entry.email %}

Feedback

Taiga has received feedback from {{ full_name }} <{{ email }}>

{% endtrans %} diff --git a/taiga/feedback/templates/emails/feedback_notification-body-text.jinja b/taiga/feedback/templates/emails/feedback_notification-body-text.jinja index 414501ae..ec2faa8b 100644 --- a/taiga/feedback/templates/emails/feedback_notification-body-text.jinja +++ b/taiga/feedback/templates/emails/feedback_notification-body-text.jinja @@ -1,4 +1,4 @@ -{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email, comment=feedback_entry.comment %}--------- +{% trans full_name=feedback_entry.full_name, email=feedback_entry.email, comment=feedback_entry.comment %}--------- - From: {{ full_name }} <{{ email }}> --------- - Comment: diff --git a/taiga/feedback/templates/emails/feedback_notification-subject.jinja b/taiga/feedback/templates/emails/feedback_notification-subject.jinja index e93fdbd1..a5fe9c3f 100644 --- a/taiga/feedback/templates/emails/feedback_notification-subject.jinja +++ b/taiga/feedback/templates/emails/feedback_notification-subject.jinja @@ -1,3 +1,3 @@ -{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email %} +{% trans full_name=feedback_entry.full_name, email=feedback_entry.email %} [Taiga] Feedback from {{ full_name }} <{{ email }}> {% endtrans %} diff --git a/taiga/front/sitemaps/issues.py b/taiga/front/sitemaps/issues.py index 38d70d70..bf5694e0 100644 --- a/taiga/front/sitemaps/issues.py +++ b/taiga/front/sitemaps/issues.py @@ -32,6 +32,9 @@ class IssuesSitemap(Sitemap): Q(project__is_private=True, project__anon_permissions__contains=["view_issues"])) + # Exclude blocked projects + queryset = queryset.filter(project__blocked_code__isnull=True) + # Project data is needed queryset = queryset.select_related("project") diff --git a/taiga/front/sitemaps/milestones.py b/taiga/front/sitemaps/milestones.py index 73a1c064..274cbaf8 100644 --- a/taiga/front/sitemaps/milestones.py +++ b/taiga/front/sitemaps/milestones.py @@ -34,6 +34,9 @@ class MilestonesSitemap(Sitemap): "view_us", "view_tasks"])) + # Exclude blocked projects + queryset = queryset.filter(project__blocked_code__isnull=True) + # Project data is needed queryset = queryset.select_related("project") diff --git a/taiga/front/sitemaps/projects.py b/taiga/front/sitemaps/projects.py index 80cd483b..cdfc8cc2 100644 --- a/taiga/front/sitemaps/projects.py +++ b/taiga/front/sitemaps/projects.py @@ -32,6 +32,9 @@ class ProjectsSitemap(Sitemap): Q(is_private=True, anon_permissions__contains=["view_project"])) + # Exclude blocked projects + queryset = queryset.filter(blocked_code__isnull=True) + return queryset def location(self, obj): diff --git a/taiga/front/sitemaps/tasks.py b/taiga/front/sitemaps/tasks.py index a1b2570e..eaa599ff 100644 --- a/taiga/front/sitemaps/tasks.py +++ b/taiga/front/sitemaps/tasks.py @@ -32,6 +32,9 @@ class TasksSitemap(Sitemap): Q(project__is_private=True, project__anon_permissions__contains=["view_tasks"])) + # Exclude blocked projects + queryset = queryset.filter(project__blocked_code__isnull=True) + # Project data is needed queryset = queryset.select_related("project") diff --git a/taiga/front/sitemaps/users.py b/taiga/front/sitemaps/users.py index 675839a0..5e956dd7 100644 --- a/taiga/front/sitemaps/users.py +++ b/taiga/front/sitemaps/users.py @@ -16,6 +16,7 @@ # along with this program. If not, see . from django.apps import apps +from django.contrib.auth import get_user_model from taiga.front.templatetags.functions import resolve @@ -24,7 +25,7 @@ from .base import Sitemap class UsersSitemap(Sitemap): def items(self): - user_model = apps.get_model("users", "User") + user_model = get_user_model() # Only active users and not system users queryset = user_model.objects.filter(is_active=True, diff --git a/taiga/front/sitemaps/userstories.py b/taiga/front/sitemaps/userstories.py index 0d59c323..9d66773c 100644 --- a/taiga/front/sitemaps/userstories.py +++ b/taiga/front/sitemaps/userstories.py @@ -32,6 +32,9 @@ class UserStoriesSitemap(Sitemap): Q(project__is_private=True, project__anon_permissions__contains=["view_us"])) + # Exclude blocked projects + queryset = queryset.filter(project__blocked_code__isnull=True) + # Project data is needed queryset = queryset.select_related("project") diff --git a/taiga/front/sitemaps/wiki.py b/taiga/front/sitemaps/wiki.py index 7021d393..85e03ba0 100644 --- a/taiga/front/sitemaps/wiki.py +++ b/taiga/front/sitemaps/wiki.py @@ -32,6 +32,9 @@ class WikiPagesSitemap(Sitemap): Q(project__is_private=True, project__anon_permissions__contains=["view_wiki_pages"])) + # Exclude blocked projects + queryset = queryset.filter(project__blocked_code__isnull=True) + # Exclude wiki pages from projects without wiki section enabled queryset = queryset.exclude(project__is_wiki_activated=False) diff --git a/taiga/front/urls.py b/taiga/front/urls.py index 76709327..e7ea3e0f 100644 --- a/taiga/front/urls.py +++ b/taiga/front/urls.py @@ -46,6 +46,7 @@ urls = { "team": "/project/{0}/team/", # project.slug - "project-admin": "/project/{0}/admin/project-profile/details", # project.slug -} + "project-transfer": "/project/{0}/transfer/{1}", # project.slug, project.transfer_token + "project-admin": "/login?next=/project/{0}/admin/project-profile/details", # project.slug +} diff --git a/taiga/hooks/api.py b/taiga/hooks/api.py index e2aea495..6590832e 100644 --- a/taiga/hooks/api.py +++ b/taiga/hooks/api.py @@ -64,6 +64,9 @@ class BaseWebhookApiViewSet(GenericViewSet): if not self._validate_signature(project, request): raise exc.BadRequest(_("Bad signature")) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + event_name = self._get_event_name(request) payload = self._get_payload(request) diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py index a4a8c00d..30d92c39 100644 --- a/taiga/hooks/bitbucket/event_hooks.py +++ b/taiga/hooks/bitbucket/event_hooks.py @@ -40,19 +40,16 @@ class PushEventHook(BaseEventHook): changes = self.payload.get("push", {}).get('changes', []) for change in filter(None, changes): - new = change.get("new", None) - if not new: + commits = change.get("commits", []) + if not commits: continue - target = new.get("target", None) - if not target: - continue + for commit in commits: + message = commit.get("message", None) + if not message: + continue - message = target.get("message", None) - if not message: - continue - - self._process_message(message, None) + self._process_message(message, None) def _process_message(self, message, bitbucket_user): """ diff --git a/taiga/hooks/bitbucket/services.py b/taiga/hooks/bitbucket/services.py index a1b12d9b..9b2b8d21 100644 --- a/taiga/hooks/bitbucket/services.py +++ b/taiga/hooks/bitbucket/services.py @@ -17,10 +17,10 @@ import uuid +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.conf import settings -from taiga.users.models import User from taiga.base.utils.urls import get_absolute_url @@ -43,4 +43,4 @@ def get_or_generate_config(project): def get_bitbucket_user(user_id): - return User.objects.get(is_system=True, username__startswith="bitbucket") + return get_user_model().objects.get(is_system=True, username__startswith="bitbucket") diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py index b4114449..830406bf 100644 --- a/taiga/hooks/github/services.py +++ b/taiga/hooks/github/services.py @@ -17,9 +17,9 @@ import uuid +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse -from taiga.users.models import User from taiga.users.models import AuthData from taiga.base.utils.urls import get_absolute_url @@ -49,6 +49,6 @@ def get_github_user(github_id): pass if user is None: - user = User.objects.get(is_system=True, username__startswith="github") + user = get_user_model().objects.get(is_system=True, username__startswith="github") return user diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py index 2b2af849..d80fe62c 100644 --- a/taiga/hooks/gitlab/services.py +++ b/taiga/hooks/gitlab/services.py @@ -17,10 +17,10 @@ import uuid +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.conf import settings -from taiga.users.models import User from taiga.base.utils.urls import get_absolute_url @@ -47,11 +47,11 @@ def get_gitlab_user(user_email): if user_email: try: - user = User.objects.get(email=user_email) - except User.DoesNotExist: + user = get_user_model().objects.get(email=user_email) + except get_user_model().DoesNotExist: pass if user is None: - user = User.objects.get(is_system=True, username__startswith="gitlab") + user = get_user_model().objects.get(is_system=True, username__startswith="gitlab") return user diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po index 27f087a7..ed198ff2 100644 --- a/taiga/locale/ca/LC_MESSAGES/django.po +++ b/taiga/locale/ca/LC_MESSAGES/django.po @@ -3,15 +3,16 @@ # This file is distributed under the same license as the taiga-back package. # # Translators: -# Javier Julián Olmos , 2015 +# Xaviju , 2015 # Taiga Dev Team , 2015 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ca/)\n" "MIME-Version: 1.0\n" @@ -41,32 +42,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Requerit. 255 caràcters o menys. Lletres, nombres i caràcters /./-/_" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "El mot d'usuari ja està en ús." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Aquest e-mail ja està en ús." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "El token no s'ajusta a cap invitació vàlida" -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Aquest usuari ja està registrat" -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Error creant un nou usuari." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Token invàlid" @@ -182,6 +184,15 @@ msgstr "" "Puja una imatge vàlida. El fitxer que has pujat no ès una imatge o el fitxer " "està corrupte." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "La página no es 'last' ni pot ser convertida a un 'int'" @@ -239,23 +250,23 @@ msgstr "" msgid "No input provided" msgstr "" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "" -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "" @@ -330,12 +341,16 @@ msgstr "Error d'integritat per argument invàlid o erroni." msgid "Precondition error" msgstr "Precondició errònia." -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "" @@ -454,71 +469,71 @@ msgstr "" " Comentari: %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Es necessita arxiu dump." -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Format d'arxiu dump invàlid" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "" @@ -537,9 +552,7 @@ msgid "It contain invalid custom fields." msgstr "Conté camps personalitzats invàlids." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "" @@ -705,12 +718,12 @@ msgstr "" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "Nom" @@ -722,11 +735,11 @@ msgstr "" msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "Descripció" @@ -740,7 +753,7 @@ msgid "secret key for ciphering the application tokens" msgstr "" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "" @@ -748,11 +761,11 @@ msgstr "" msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "Nom complet" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "Adreça d'email" @@ -760,11 +773,11 @@ msgstr "Adreça d'email" msgid "comment" msgstr "Comentari" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -836,8 +849,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "El payload no és un arxiu json vàlid" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "El projecte no existeix" @@ -845,26 +858,26 @@ msgstr "El projecte no existeix" msgid "Bad signature" msgstr "Firma no vàlida." -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "L'element referenciat no existeix" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "L'estatus no existeix." -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Informació d'incidència no vàlida." -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -875,17 +888,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Informació del comentari a l'incidència no vàlid." -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -896,7 +909,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1135,96 +1148,110 @@ msgstr "Administrar valors de projecte" msgid "Admin roles" msgstr "Administrar rols" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Arguments incomplets." -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Format d'image invàlid" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Al menys un del usuaris ha de ser administrador" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "No tens permisos per a veure açò." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "Amo" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "Projecte" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "Tipus de contingut" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "Id d'objecte" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "Data de modificació" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "Arxiu adjunt" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "està obsolet " -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "Ordre" @@ -1244,18 +1271,34 @@ msgstr "" msgid "Talky" msgstr "" -#: taiga/projects/custom_attributes/choices.py:26 -msgid "Text" +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" msgstr "" #: taiga/projects/custom_attributes/choices.py:27 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1354,7 +1397,7 @@ msgstr "Borrat" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Sense assignar" @@ -1415,23 +1458,23 @@ msgstr "nota de bloqueig" msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "No tens permissos per a ficar aquest sprint a aquesta incidència" -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "No tens permissos per a ficar aquest status a aquesta tasca" -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "No tens permissos per a ficar aquesta severitat a aquesta tasca" -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "No tens permissos per a ficar aquesta prioritat a aquesta incidència" -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "No tens permissos per a ficar aquest tipus a aquesta incidència" @@ -1483,29 +1526,29 @@ msgstr "M'agrada" #: taiga/projects/likes/models.py:36 msgid "Likes" -msgstr "" +msgstr "Fans" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "Data estimada d'inici" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "Data estimada de finalització" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "està tancat" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibilitat" @@ -1530,224 +1573,232 @@ msgstr "" msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "email" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "text extra d'invitació" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "L'usuari ja es membre del projecte" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "Points per defecte" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "estatus d'història d'usuai per defecte" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "Estatus de tasca per defecte" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "Prioritat per defecte" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "Severitat per defecte" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "Status d'incidència per defecte" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "Tipus d'incidència per defecte" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "membres" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "total de fites" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "total de punts d'història" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "activa panell de backlog" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "activa panell de kanban" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "activa panell de wiki" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "activa panell d'incidències" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "sistema de videoconferència" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "template de creació" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "permisos d'anònims" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "permisos d'usuaris" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "es privat" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "colors de tags" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "Actualitzada data" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "configuració de mòdules" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "està arxivat" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "color" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "limit de treball en progrés" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "valor" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "rol d'amo per defecte" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "opcions per defecte" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "status d'històries d'usuari" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "punts" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "status de tasques" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "status d'incidències" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "tipus d'incidències" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "prioritats" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "severitats" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "rols" @@ -1763,29 +1814,29 @@ msgstr "" msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "creada data" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "" @@ -2287,54 +2338,63 @@ msgid "version" msgstr "Versió" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "No pots deixar el projecte si no hi ha més amos" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Aquest e-mail ja està en ús" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Rol invàlid per al projecte" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Opcions per defecte" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Estatus d'històries d'usuari" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Punts" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Estatus de tasques" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Estatus d'incidéncies" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Tipus d'incidéncies" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Prioritats" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Severitats" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Rols" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "" @@ -2342,15 +2402,26 @@ msgstr "" msgid "Project End" msgstr "" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Token invàlid" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2496,6 +2567,236 @@ msgstr "" "\n" "[Taiga] Afegit al projecte '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -2728,15 +3029,15 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -2795,11 +3096,11 @@ msgstr "Vots" msgid "Vote" msgstr "Vot" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "" @@ -2811,7 +3112,7 @@ msgstr "últim a modificar" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "" @@ -2819,65 +3120,65 @@ msgstr "" msgid "Personal info" msgstr "Informació personal" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Permissos" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Dates importants" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Email duplicat" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Email no vàlid" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Nom d'usuari o email invàlid" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Correu enviat satisfactòriament" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Token invàlid" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Paràmetre de password actual requerit" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Paràmetre de password requerit" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Password invàlid, al menys 6 caràcters requerits" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Password actual invàlid" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Invàlid. Estás segur que el token es correcte i que no l'has usat abans?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Invàlid. Estás segur que el token es correcte?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "estatus de superusuari" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -2885,24 +3186,24 @@ msgstr "" "Designa que aquest usuari te tots els permisos sense asignarli-los " "explícitament." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "mot d'usuari" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Requerit. 30 caràcters o menys. Lletres, nombres i caràcters /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Introdueix un nom d'usuari vàlid" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "actiu" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2910,43 +3211,59 @@ msgstr "" "Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en " "lloc de borrar el compte." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "foto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "data d'unió" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "llenguatge per defecte" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "zona horaria per defecte" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "coloritza tags" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "token de correu" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "nova adreça de correu" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "permissos" @@ -2958,10 +3275,26 @@ msgstr "invàlid" msgid "Invalid username. Try with a different one." msgstr "Nom d'usuari invàlid" -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "" +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po index 19f5adfd..2fde1a47 100644 --- a/taiga/locale/de/LC_MESSAGES/django.po +++ b/taiga/locale/de/LC_MESSAGES/django.po @@ -17,9 +17,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/de/)\n" "MIME-Version: 1.0\n" @@ -51,32 +52,33 @@ msgstr "" "255 oder weniger Zeichen aus Buchstaben, Zahlen und Punkt, Minus oder " "Unterstrich erforderlich." -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Der Benutzername wird schon verwendet." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Diese E-Mail Adresse wird schon verwendet." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Der Benutzer ist schon registriert." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Fehler bei der Erstellung des neuen Benutzers." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Ungültiges Token" @@ -212,6 +214,15 @@ msgstr "" "Bitte laden Sie ein gültiges Bild hoch. Die Datei, die Sie hochgeladen " "haben, ist entweder kein Bild oder defekt." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Seite ist nicht 'letzte', noch kann diese konvertiert werden." @@ -269,25 +280,25 @@ msgstr "Ungültige Daten" msgid "No input provided" msgstr "Es gab keine Eingabe" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Es können nur existierende Einträge aktualisiert werden. Eine Neuerstellung " "ist nicht möglich." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Es wurde eine Liste von Einträgen erwartet." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Nicht gefunden." -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Zugriff verweigert" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Fehler bei der Serveranmeldung" @@ -362,12 +373,16 @@ msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente" msgid "Precondition error" msgstr "Voraussetzungsfehler" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Fehler in Filter Parameter Typen." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' muss ein Integer-Wert sein." @@ -509,71 +524,71 @@ msgstr "" "Kommentar: %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Es ist mindestens eine Rolle nötig" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Exportdatei erforderlich" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Ungültiges Exportdatei Format" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "Fehler beim Importieren der Projektdaten" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "Fehler beim Importieren der Listen von Projektattributen" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte " -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "Fehler beim Importieren der Kundenattribute" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "Fehler beim Importieren der Rollen" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "Fehler beim Importieren der Mitgliedschaften" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "Fehler beim Import der Sprints" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "Fehler beim Importieren von Wiki Seiten" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "Fehler beim Importieren von Wiki Links" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "Fehler beim Importieren der Tickets" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "Fehler beim Importieren der User-Stories" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "Fehler beim Importieren der Aufgaben" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "Fehler beim Importieren der Schlagworte" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "Fehler beim Importieren der Chroniken" @@ -592,9 +607,7 @@ msgid "It contain invalid custom fields." msgstr "Enthält ungültige Benutzerfelder." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Der Name für das Projekt ist doppelt vergeben" @@ -853,12 +866,12 @@ msgstr "Authentifizierung erforderlich" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "Name" @@ -870,11 +883,11 @@ msgstr "Icon URL" msgid "web" msgstr "Web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "Beschreibung" @@ -888,7 +901,7 @@ msgid "secret key for ciphering the application tokens" msgstr "Geheimer Schlüssel für Verschlüsselung der Anwensungs-Token" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "Benutzer" @@ -896,11 +909,11 @@ msgstr "Benutzer" msgid "application" msgstr "Applikation" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "vollständiger Name" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "E-Mail Adresse" @@ -908,11 +921,11 @@ msgstr "E-Mail Adresse" msgid "comment" msgstr "Kommentar" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -984,8 +997,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Die Nutzlast ist kein gültiges json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Das Projekt existiert nicht" @@ -993,26 +1006,26 @@ msgstr "Das Projekt existiert nicht" msgid "Bad signature" msgstr "Falsche Signatur" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Das referenzierte Element existiert nicht" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Der Status existiert nicht" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Der Status des BitBucket Commits hat sich geändert" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Ungültige Ticket-Information" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1029,17 +1042,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Ticket erstellt von BitBucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Ungültige Ticket-Kommentar Information" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1056,7 +1069,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1325,96 +1338,110 @@ msgstr "Administrator Projekt Werte" msgid "Admin roles" msgstr "Administrator-Rollen" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Unvollständige Argumente" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Ungültiges Bildformat" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Unglültiger Templatename" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Ungültige Templatebeschreibung" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Mindestens ein Benutzer muss ein aktiver Administrator sein. " +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Sie haben keine Berechtigungen für diese Ansicht" -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Teil-Aktualisierungen sind nicht unterstützt" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "Besitzer" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "Projekt" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "Inhaltsart" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "Objekt Nr." -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "Zeitpunkt der Änderung" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "Angehangene Datei" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "SHA1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "wurde verworfen" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "Reihenfolge" @@ -1434,18 +1461,34 @@ msgstr "Kunde" msgid "Talky" msgstr "Gesprächig" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Text" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Mehrzeiliger Text" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Datum" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1544,7 +1587,7 @@ msgstr "entfernt" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Nicht zugewiesen" @@ -1605,27 +1648,27 @@ msgstr "Blockierungsgrund" msgid "sprint" msgstr "Sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diesen Sprint zu setzen." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diesen Status zu setzen. " -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diese Gewichtung zu setzen." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diese Priorität zu setzen. " -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Sie haben nicht die Berechtigung, das Ticket auf diese Art zu setzen." @@ -1679,27 +1722,27 @@ msgstr "Like" msgid "Likes" msgstr "Likes" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "Slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "geschätzter Starttermin" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "geschätzter Endtermin" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "ist geschlossen" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "Verfügbarkeit" @@ -1724,224 +1767,232 @@ msgstr "'{param}' Parameter ist ein Pflichtfeld" msgid "'project' parameter is mandatory" msgstr "Der 'project' Parameter ist ein Pflichtfeld" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "E-Mail" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "erstellt am " -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "Token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "Einladung Zusatztext " -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "Benutzerreihenfolge" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "Der Benutzer ist bereits Mitglied dieses Projekts" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "voreingestellte Punkte" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "voreingesteller User-Story Status " -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "voreingestellter Aufgabenstatus" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "voreingestellte Priorität " -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "voreingestellte Gewichtung " -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "voreingestellter Ticket Status" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "voreingestellter Ticket Typ" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "Mitglieder" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "Meilensteine Gesamt" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "Story Punkte insgesamt" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "aktives Backlog Panel" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "aktives Kanban Panel" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "aktives Wiki Panel" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "aktives Tickets Panel" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "Videokonferenzsystem" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "Zusatzdaten Videokonferenz" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "Vorlage erstellen" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "Rechte für anonyme Nutzer" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "Rechte für registrierte Nutzer" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "ist privat" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "Tag Farben" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "Aktualisierungsdatum" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "Count" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "Module konfigurieren" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "ist archiviert" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "Farbe" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "Ausführungslimit" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "Wert" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "voreingestellte Besitzerrolle" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "Vorgabe Optionen" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "User-Story Status " -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "Punkte" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "Aufgaben Status" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "Ticket Status" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "Ticket Arten" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "Prioritäten" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "Gewichtung" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "Rollen" @@ -1957,29 +2008,29 @@ msgstr "Alle" msgid "None" msgstr "Keine" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "Erstelldatum" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "Chronik Einträge" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "Benutzer benachrichtigen" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Beobachtet" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Benachrichtigung für bestimmte Benutzer und Projekt aktiviert" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Ungültiger Wert für Benachrichtigungslevel" @@ -2759,56 +2810,63 @@ msgid "version" msgstr "Version" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" msgstr "" -"Sie können das Projekt nicht verlassen, wenn keine weiteren Besitzer " -"vorhanden sind" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Die E-Mailadresse ist bereits vergeben" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Ungültige Rolle für dieses Projekt" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Voreingestellte Optionen" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Status für User-Stories" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Punkte" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Aufgaben Status" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Ticket Status" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Ticket Arten" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Prioritäten" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Gewichtung" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Rollen" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Zukünftiger Sprint" @@ -2816,18 +2874,29 @@ msgstr "Zukünftiger Sprint" msgid "Project End" msgstr "Projektende" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Token ist ungültig" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "" "Sie haben nicht die Berechtigung, diesen Sprint auf diese Aufgabe zu setzen" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" "Sie haben nicht die Berechtigung, diese User-Story auf diese Aufgabe zu " "setzen" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "" "Sie haben nicht die Berechtigung, diesen Status auf diese Aufgabe zu setzen." @@ -3006,6 +3075,236 @@ msgstr "" " [Taiga] Zum Projekt hinzugefügt '%(project)s'\n" " \n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3249,19 +3548,19 @@ msgstr "Projekteigentümer " msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Sie haben nicht die Berechtigung, diesen Sprint auf diese User-Story zu " "setzen." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" "Sie haben nicht die Berechtigung, diesen Status auf diese User-Story zu " "setzen." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Erstelle die User-Story #{ref} - {subject}" @@ -3320,11 +3619,11 @@ msgstr "Stimmen" msgid "Vote" msgstr "Stimme" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'content' Parameter ist erforderlich" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id' Parameter ist erforderlich" @@ -3336,7 +3635,7 @@ msgstr "letzte Änderung" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Prüfe die API der Historie auf Übereinstimmung" @@ -3344,66 +3643,66 @@ msgstr "Prüfe die API der Historie auf Übereinstimmung" msgid "Personal info" msgstr "Personal Information" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Berechtigungen" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Wichtige Termine" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Doppelte E-Mail" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Ungültige E-Mail" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Ungültiger Benutzername oder E-Mail" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "E-Mail erfolgreich gesendet." -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Token ist ungültig" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Aktueller Passwort Parameter wird benötigt" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Neuer Passwort Parameter benötigt" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Ungültige Passwortlänge, mindestens 6 Zeichen erforderlich" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Ungültiges aktuelles Passwort" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Ungültig. Sind Sie sicher, dass das Token korrekt ist und Sie es nicht " "bereits verwendet haben?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Ungültig. Sind Sie sicher, dass das Token korrekt ist?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "Superuser Status" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3411,25 +3710,25 @@ msgstr "" "Dieser Benutzer soll alle Berechtigungen erhalten, ohne dass diese zuvor " "zugewiesen werden müssen. " -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "Benutzername" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Benötigt. 30 Zeichen oder weniger.. Buchstaben, Zahlen und /./-/_ Zeichen" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Geben Sie einen gültigen Benuzternamen ein." -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "aktiv" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3437,43 +3736,59 @@ msgstr "" "Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen " "Benutzer zu löschen." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "Über mich" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "Foto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "Beitrittsdatum" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "Vorgegebene Sprache" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "Standard-Theme" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "Vorgegebene Zeitzone" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "Tag-Farben" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "E-Mail Token" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "neue E-Mail Adresse" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "Berechtigungen" @@ -3485,10 +3800,26 @@ msgstr "ungültig" msgid "Invalid username. Try with a different one." msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index 7e445d3f..6d00fcbd 100644 --- a/taiga/locale/en/LC_MESSAGES/django.po +++ b/taiga/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -37,32 +37,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "" -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "" -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "" -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "" -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "" #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "" @@ -174,6 +175,15 @@ msgid "" "corrupted image." msgstr "" +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" @@ -231,23 +241,23 @@ msgstr "" msgid "No input provided" msgstr "" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "" -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "" @@ -322,12 +332,16 @@ msgstr "" msgid "Precondition error" msgstr "" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "" @@ -443,71 +457,71 @@ msgid "" " " msgstr "" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "" @@ -526,9 +540,7 @@ msgid "It contain invalid custom fields." msgstr "" #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "" @@ -694,12 +706,12 @@ msgstr "" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "" @@ -711,11 +723,11 @@ msgstr "" msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "" @@ -729,7 +741,7 @@ msgid "secret key for ciphering the application tokens" msgstr "" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "" @@ -737,11 +749,11 @@ msgstr "" msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "" @@ -749,11 +761,11 @@ msgstr "" msgid "comment" msgstr "" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -809,8 +821,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "" @@ -818,26 +830,26 @@ msgstr "" msgid "Bad signature" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -848,17 +860,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -869,7 +881,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1108,96 +1120,110 @@ msgstr "" msgid "Admin roles" msgstr "" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" +#: taiga/projects/api.py:356 +msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "" -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "" @@ -1217,18 +1243,34 @@ msgstr "" msgid "Talky" msgstr "" -#: taiga/projects/custom_attributes/choices.py:26 -msgid "Text" +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" msgstr "" #: taiga/projects/custom_attributes/choices.py:27 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1327,7 +1369,7 @@ msgstr "" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "" @@ -1388,23 +1430,23 @@ msgstr "" msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "" -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "" -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "" -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "" -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "" @@ -1458,27 +1500,27 @@ msgstr "" msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "" @@ -1503,224 +1545,232 @@ msgstr "" msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "" @@ -1736,29 +1786,29 @@ msgstr "" msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "" @@ -2254,54 +2304,63 @@ msgid "version" msgstr "" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "" @@ -2309,15 +2368,26 @@ msgstr "" msgid "Project End" msgstr "" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2445,6 +2515,236 @@ msgid "" "[Taiga] Added to the project '%(project)s'\n" msgstr "" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -2677,15 +2977,15 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -2744,11 +3044,11 @@ msgstr "" msgid "Vote" msgstr "" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "" @@ -2760,7 +3060,7 @@ msgstr "" msgid "href" msgstr "" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "" @@ -2768,129 +3068,145 @@ msgstr "" msgid "Personal info" msgstr "" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "" @@ -2902,10 +3218,26 @@ msgstr "" msgid "Invalid username. Try with a different one." msgstr "" -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "" +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index f3e064b4..f337481a 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -3,19 +3,23 @@ # This file is distributed under the same license as the taiga-back package. # # Translators: -# David Barragán , 2015 +# David Barragán , 2015-2016 # Esther Moreno , 2015 # gustavodiazjaimes , 2015 # Hector Colina , 2015 # Jesus Marin , 2015 +# Luis Sebastian Urrutia Fuentes , 2016 +# Renelis Abreu Ramirez , 2016 # Taiga Dev Team , 2015-2016 +# Xaviju , 2016 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-23 21:41+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/es/)\n" "MIME-Version: 1.0\n" @@ -45,32 +49,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Son necesarios. 255 caracteres o menos (letras, números y /./-/_)" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Nombre de usuario no disponible" -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Email no disponible" -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "El token no pertenece a ninguna invitación válida." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Este usuario ya está registrado." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "Este usuario ya es miembro del proyecto." -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Error al crear un nuevo usuario " #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Token inválido" @@ -196,6 +201,15 @@ msgid "" "corrupted image." msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "Elemento bloqueado" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "La página no es 'last' o no es un número." @@ -254,25 +268,25 @@ msgstr "Datos invalidos" msgid "No input provided" msgstr "No se han introducido datos." -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "No se pueden crear nuevos objetos. Sólo está permitida la actualización de " "los existentes." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Se esperaba una lista de objetos." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "No encontrado" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Permiso denegado." -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Error en la aplicación del servidor." @@ -347,12 +361,16 @@ msgstr "Error de integridad por argumentos incorrectos o inválidos" msgid "Precondition error" msgstr "Error por incumplimiento de precondición" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Error en los típos de parámetros de filtrado" -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' debe ser un valor entero." @@ -493,71 +511,71 @@ msgstr "" "\n" "Comentario: %(comment)s" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Necesitamos al menos un rol" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Se necesita el fichero con los datos exportados" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Formato de fichero de exportación inválido" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "error importando los datos del proyecto" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "error importando la listados de valores de attributos del proyecto" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "error importando los valores por defecto de los atributos del proyecto" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "error importando los atributos personalizados" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "error importando los roles" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "error importando los miembros" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "error importando los sprints" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "error importando las páginas del wiki" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "error importando los enlaces del wiki" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "error importando las peticiones" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "error importando las historias de usuario" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "error importando las tareas" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "error importando las etiquetas" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "error importando los timelines" @@ -576,9 +594,7 @@ msgid "It contain invalid custom fields." msgstr "Contiene attributos personalizados inválidos." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Nombre duplicado para el proyecto" @@ -608,8 +624,8 @@ msgstr "" "\n" "

Volca de datos de proyecto generado

\n" "

Hola %(user)s,

\n" -"

El volcado de datos de tu proyecto %(project)s se ha generado con " -"éxisito.

\n" +"

El volcado de datos de tu proyecto %(project)s se ha generado con éxito." +"

\n" "

Puedes descargarlo aquí:

\n" "Descargar el archivo con el volcado de datos\n" @@ -635,7 +651,7 @@ msgstr "" "\n" "Hola %(user)s,\n" "\n" -"El volcado de datos de tu proyecto %(project)s se ha generado con éxisito. " +"El volcado de datos de tu proyecto %(project)s se ha generado con éxito. " "Puedes descargarlo aquí:\n" "\n" "%(url)s\n" @@ -832,12 +848,12 @@ msgstr "Se requiere autenticación" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "nombre" @@ -849,11 +865,11 @@ msgstr "URL del icono" msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "descripción" @@ -867,7 +883,7 @@ msgid "secret key for ciphering the application tokens" msgstr "clave secreta para cifrar los tokens de aplicación" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "usuario" @@ -875,11 +891,11 @@ msgstr "usuario" msgid "application" msgstr "aplicación" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "nombre completo" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "dirección de email" @@ -887,11 +903,11 @@ msgstr "dirección de email" msgid "comment" msgstr "comentario" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -962,8 +978,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "El payload no es un json válido" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "El proyecto no existe" @@ -971,26 +987,26 @@ msgstr "El proyecto no existe" msgid "Bad signature" msgstr "Firma errónea" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "El elemento referenciado no existe" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "El estado no existe" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Estado cambiado desde un commit de BitBucket" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Información inválida de Issue" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1007,17 +1023,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Petición creada desde BitBucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Información de comentario de Issue inválida" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1034,7 +1050,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1302,96 +1318,112 @@ msgstr "Administrar valores de proyecto" msgid "Admin roles" msgstr "Administrar roles" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Argumentos incompletos" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Formato de imagen no válido" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Nombre de plantilla invalido" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Descripción de plantilla invalida" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Al menos uno de los usuario debe ser un administrador." +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "id de usuario inválido" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "El usuario no existe" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" +"El proyecto debe tener un dueño y al menos uno de los usuarios debe ser un " +"administrador activo" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "No tienes suficientes permisos para ver esto." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "La actualización parcial no está soportada." -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "Dueño" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "Proyecto" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "típo de contenido" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "id de objeto" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "fecha modificada" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "archivo adjunto" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "está desactualizado" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "orden" @@ -1411,18 +1443,34 @@ msgstr "Personalizado" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Texto" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Texto multilínea" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Fecha" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "Url" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1521,7 +1569,7 @@ msgstr "borrado" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "No asignado" @@ -1582,23 +1630,23 @@ msgstr "nota de bloqueo" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "No tienes permisos para asignar un sprint a esta petición." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "No tienes permisos para asignar un estado a esta petición." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "No tienes permisos para establecer la gravedad de esta petición." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "No tienes permiso para establecer la prioridad de esta petición." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "No tienes permiso para establecer el tipo de esta petición." @@ -1652,27 +1700,27 @@ msgstr "Like" msgid "Likes" msgstr "Likes" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "fecha estimada de comienzo" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "fecha estimada de finalización" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "está cerrada" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibilidad" @@ -1699,224 +1747,232 @@ msgstr "el parámetro '{param}' es obligatório" msgid "'project' parameter is mandatory" msgstr "el parámetro 'project' es obligatório" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "email" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "creado el" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "texto extra de la invitación" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "orden del usuario" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "El usuario ya es miembro del proyecto" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "puntos por defecto" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "estado de historia por defecto" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "estado de tarea por defecto" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "prioridad por defecto" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "gravedad por defecto" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "estado de petición por defecto" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "tipo de petición por defecto" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "miembros" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "total de sprints" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "puntos de historia totales" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "panel de backlog activado" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "panel de kanban activado" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "panel de wiki activo" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "panel de peticiones activo" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "sistema de videoconferencia" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "datos extra de videoconferencia" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "creación de plantilla" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "permisos de anónimo" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "permisos de usuario" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "privado" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "es destacado" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "está buscando a gente" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "nota (buscando a gente)" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "colores de etiquetas" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "token de transferencia de proyecto" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "código bloqueado" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "fecha y hora de actualización" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "recuento" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "fans la última semana" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "fans el último mes" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "fans el último año" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "actividad la última semana" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "actividad el último mes" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "actividad el último áño" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "configuración de modulos" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "archivado" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "color" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "limite del trabajo en progreso" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "valor" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "rol por defecto para el propietario" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "opciones por defecto" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "estatuas de historias" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "puntos" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "estatus de tareas" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "estados de petición" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "tipos de petición" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "gravedades" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "roles" @@ -1932,30 +1988,30 @@ msgstr "Todas" msgid "None" msgstr "Ninguna" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "fecha y hora de creación" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "entradas del histórico" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "usuarios notificados" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Observado" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "" "Ya existe una política de notificación para este usuario en el proyecto." -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Valor inválido para el nivel de notificación" @@ -2687,55 +2743,65 @@ msgid "version" msgstr "versión" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" msgstr "" -"No puedes abandonar este proyecto si no existen mas propietarios del mismo" +"No puedes abandonar el proyecto si eres el dueño o no existen más " +"administradores" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "La dirección de email ya está en uso." -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Rol inválido para el proyecto" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Opciones por defecto" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Estados de historia de usuario" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Puntos" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Estado de tareas" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Estados de peticion" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Tipos de petición" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Prioridades" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Gravedades" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Roles" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Sprint futuro" @@ -2743,15 +2809,26 @@ msgstr "Sprint futuro" msgid "Project End" msgstr "Final de proyecto" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "token inválido" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "El token ha expirado" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "No tienes permisos para asignar este sprint a esta tarea." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "No tienes permisos para asignar esta historia a esta tarea." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "No tienes permisos para asignar este estado a esta tarea." @@ -2917,6 +2994,256 @@ msgstr "" "\n" "[Taiga] Añadido al proyecto '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" +"\n" +"Hola %(old_owner_name)s,\n" +"%(new_owner_name)s ha aceptado tu proposición y será el nuevo dueño de " +"\"%(project_name)s\".\n" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" +"\n" +"De ahora en adelante, tu rol para este proyecto será de \"admin\".\n" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" +"\n" +"El Equipo de Taiga\n" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" +"\n" +"[%(project)s] ¡Proposición de transferencia de dueño aceptada!\n" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "Solicitar transferir a una persona diferente" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" +"\n" +"Hola %(owner_name)s,\n" +"%(rejecter_name)s ha declinado tu oferta y no será el dueño del nuevo " +"proyecto \"%(project_name)s\".\n" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" +"\n" +"Si deseas, todavía puedes intentar transferir la propiedad del proyecto a " +"una persona diferente.\n" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "Solicitar transferir a una persona diferente:" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" +"\n" +"[%(project)s] Propiedad de transferencia de proyecto rechazada\n" +"\n" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "Continuar" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3163,17 +3490,17 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "No tienes permisos para asignar este sprint a esta historia de usuario." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" "No tienes permisos para asignar este estado a esta historia de usuario." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Generada la historia de usuario #{ref} - {subject}" @@ -3232,11 +3559,11 @@ msgstr "Votos" msgid "Vote" msgstr "Voto" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "el parámetro 'content' es obligatório" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "el parámetro 'project_id' es obligatório" @@ -3248,7 +3575,7 @@ msgstr "última modificación por" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Comprueba la API de histórico para obtener el diff exacto" @@ -3256,65 +3583,65 @@ msgstr "Comprueba la API de histórico para obtener el diff exacto" msgid "Personal info" msgstr "Información personal" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Permisos" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "Restricciones" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "datos importántes" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Email duplicado" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Email no válido" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Nombre de usuario o email no válidos" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "¡Correo enviado con éxito!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "token inválido" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "La contraseña actual es obligatoria." -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "La nueva contraseña es obligatoria" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "La longitud de la contraseña debe de ser de al menos 6 caracteres" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Contraseña actual inválida" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Invalido, ¿estás seguro de que el token es correcto y no se ha usado antes?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Inválido, ¿estás seguro de que el token es correcto?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "es superusuario" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3322,24 +3649,24 @@ msgstr "" "Otorga todos los permisos a este usuario sin necesidad de hacerlo " "explicitamente." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "nombre de usuario" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Obligatorio. 30 caracteres o menos. Letras, números y /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Introduce un nombre de usuario válido" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "activo" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3347,43 +3674,59 @@ msgstr "" "Denota a los usuarios activos. Desmárcalo para dar de baja/borrar a un " "usuario." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografía" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "foto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "fecha de registro" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "idioma por defecto" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "tema por defecto" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "zona horaria por defecto" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "añade color a las etiquetas" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "token de email" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "nueva dirección de email" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "máximo de membresías para cada proyecto privado poseído" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "máximo de membresías para cada proyecto público poseído" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "permisos" @@ -3395,10 +3738,26 @@ msgstr "no válido" msgid "Invalid username. Try with a different one." msgstr "Nombre de usuario inválido. Prueba con otro." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Nombre de usuario o contraseña inválidos." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "No puedes tener más proyectos privados" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "No puedes tener más proyectos públicos" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po index 525eb7d7..b8031f95 100644 --- a/taiga/locale/fi/LC_MESSAGES/django.po +++ b/taiga/locale/fi/LC_MESSAGES/django.po @@ -9,9 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fi/)\n" "MIME-Version: 1.0\n" @@ -42,32 +43,33 @@ msgid "" msgstr "" "Vaaditaan. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Käyttäjänimi on varattu." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Sähköposti on jo varattu." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Tunniste ei vastaa mihinkään avoimeen kutsuun." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Käyttäjä on jo rekisteröitynyt." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Virhe käyttäjän luonnissa." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Väärä tunniste" @@ -184,6 +186,15 @@ msgstr "" "Anna kelvollinen kuva. Annettu ei ollut tunnistettava kuva tai se oli " "vioittunut." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Sivu ei ole 'viimeinen', ekä sitä pystytä muuntamaan numeroksi." @@ -241,23 +252,23 @@ msgstr "Virheellinen data" msgid "No input provided" msgstr "Syöte puuttuu" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "En voi luoda uutta kohdetta, vain olemassaolevat voidaan päivittää." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Anna lista kohteista." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Ei löytynyt" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Ei käyttöoikeutta" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Palvelinsovelluksen virhe" @@ -332,12 +343,16 @@ msgstr "Integrity Error for wrong or invalid arguments" msgid "Precondition error" msgstr "Precondition error" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Error in filter params types." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' must be an integer value." @@ -479,71 +494,71 @@ msgstr "" "\n" "Kommentti: %(comment)s" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Tarvitaan tiedosto" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Virheellinen tiedostomuoto" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "virhe projektidatan tuonnissa" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "virhe atribuuttilistan tuonnissa" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "virhe oletusarvojen tuonnissa" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "virhe omien arvojen tuonnissa" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "virhe roolien tuonnissa" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "virhe jäsenyyksien tuonnissa" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "virhe kierroksien tuonnissa" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "virhe wiki-sivujen tuonnissa" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "virhe viki-linkkien tuonnissa" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "virhe pyyntöjen tuonnissa" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "virhe käyttäjätarinoiden tuonnissa" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "virhe tehtävien tuonnissa" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "virhe avainsanojen sisäänlukemisessa" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "virhe aikajanojen tuonnissa" @@ -562,9 +577,7 @@ msgid "It contain invalid custom fields." msgstr "Sisältää vieheellisiä omia kenttiä." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Nimi on tuplana projektille" @@ -815,12 +828,12 @@ msgstr "" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "nimi" @@ -832,11 +845,11 @@ msgstr "" msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "kuvaus" @@ -850,7 +863,7 @@ msgid "secret key for ciphering the application tokens" msgstr "" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "" @@ -858,11 +871,11 @@ msgstr "" msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "koko nimi" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "sähköpostiosoite" @@ -870,11 +883,11 @@ msgstr "sähköpostiosoite" msgid "comment" msgstr "kommentti" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -947,8 +960,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "The payload is not a valid json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Projektia ei löydy" @@ -956,26 +969,26 @@ msgstr "Projektia ei löydy" msgid "Bad signature" msgstr "Virheellinen allekirjoitus" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Viitattu elementtiä ei löydy" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Tilaa ei löydy" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Tila muutettu BitBucket kommitilla" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Virheellinen pyynnön tieto" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -986,17 +999,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Virheellinen pyynnön kommentin tieto" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1007,7 +1020,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1264,96 +1277,110 @@ msgstr "Hallinnoi projektin arvoja" msgid "Admin roles" msgstr "Hallinnoi rooleja" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Puutteelliset argumentit" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Väärä kuvaformaatti" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Virheellinen mallipohjan nimi" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Virheellinen mallipohjan kuvaus" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Vähintään yhden käyttäjän pitää olla aktiivinen ylläpitäjä" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Sinulla ei ole oikeuksia nähdä tätä." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "Projekti ID ei vastaa kohdetta ja projektia" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "omistaja" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "projekti" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "sisältötyyppi" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "objekti ID" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "muokkauspvm" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "liite" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "on poistettu" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "order" @@ -1373,18 +1400,34 @@ msgstr "" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 -msgid "Text" +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" msgstr "" #: taiga/projects/custom_attributes/choices.py:27 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1483,7 +1526,7 @@ msgstr "poistettu" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Tekijä puuttuu" @@ -1544,23 +1587,23 @@ msgstr "suljettu muistiinpano" msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Sinulla ei ole oikeuksia laittaa kierrosta tälle pyynnölle." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Sinulla ei ole oikeutta asettaa statusta tälle pyyntö." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Sinulla ei ole oikeutta asettaa vakavuutta tälle pyynnölle." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Sinulla ei ole oikeutta asettaa kiireellisyyttä tälle pyynnölle." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Sinulla ei ole oikeutta asettaa tyyppiä tälle pyyntö." @@ -1614,27 +1657,27 @@ msgstr "" msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "hukka-aika" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "arvioitu alkupvm" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "arvioitu loppupvm" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "on suljettu" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibility" @@ -1659,224 +1702,232 @@ msgstr "'{param}' parametri on pakollinen" msgid "'project' parameter is mandatory" msgstr "'project' parametri on pakollinen" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "sähköposti" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "luo täällä" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "tunniste" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "kutsun lisäteksti" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "käyttäjäjärjestys" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "Käyttäjä on jo projektin jäsen" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "oletuspisteet" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "oletus Kt tila" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "oletus tehtävän tila" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "oletus kiireellisyys" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "oletus vakavuus" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "oletus pyynnön tila" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "oletus pyyntö tyyppi" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "jäsenet" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "virstapyväitä yhteensä" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "käyttäjätarinan yhteispisteet" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "aktiivinen odottavien paneeli" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "aktiivinen kanban-paneeli" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "aktiivinen wiki-paneeli" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "aktiivinen pyyntöpaneeli" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "videokokous järjestelmä" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "luo mallipohja" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "vieraan oikeudet" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "käyttäjän oikeudet" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "on yksityinen" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "avainsanojen värit" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "päivityspvm" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "moduulien asetukset" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "on arkistoitu" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "väri" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "työn alla olevien max" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "arvo" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "oletus omistajan rooli" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "oletus optiot" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "kt tilat" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "pisteet" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "tehtävän tilat" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "pyyntöjen tilat" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "pyyntötyypit" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "kiireellisyydet" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "vakavuudet" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "roolit" @@ -1892,29 +1943,29 @@ msgstr "" msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "luontipvm" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "historian kohteet" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "ilmoita käyttäjille" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Ilmoita olemassaolosta määritellyille käyttäjille ja projektille" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "" @@ -2656,54 +2707,63 @@ msgid "version" msgstr "versio" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Et voi jättää projektia, jos olet ainoa omistaja" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Sähköpostiosoite on jo käytössä" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Virheellinen rooli projektille" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Oletusoptiot" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Käyttäjätarinatilat" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Pisteet" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Tehtävien tilat" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Pyyntöjen tilat" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "pyyntötyypit" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Kiireellisyydet" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Vakavuudet" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Roolit" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Tuleva kierros" @@ -2711,15 +2771,26 @@ msgstr "Tuleva kierros" msgid "Project End" msgstr "Projektin loppu" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Tunniste on virheellinen" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2887,6 +2958,236 @@ msgstr "" "\n" "[Taiga] Lisätty projektiin '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3128,15 +3429,15 @@ msgstr "Tuoteomistaja" msgid "Stakeholder" msgstr "Sidosryhmä" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -3195,11 +3496,11 @@ msgstr "Ääniä" msgid "Vote" msgstr "Äänestä" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'content' parametri on pakollinen" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametri on pakollinen" @@ -3211,7 +3512,7 @@ msgstr "viimeksi muokannut" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "" @@ -3219,135 +3520,151 @@ msgstr "" msgid "Personal info" msgstr "Henkilökohtaiset tiedot" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Oikeudet" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Tärkeät päivämäärät" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Sähköposti on jo olemassa" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Virheellinen sähköposti" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Tuntematon käyttäjänimi tai sähköposti" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Sähköposti lähetetty." -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Tunniste on virheellinen" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Nykyinen salasanaparametri tarvitaan" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Uusi salasanaparametri tarvitaan" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Salasanan pitää olla vähintään 6 merkkiä pitkä" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Virheellinen nykyinen salasana" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Virheellinen. Oletko varma, että tunniste on oikea ja et ole jo käyttänyt " "sitä?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Virheellinen, oletko varma että tunniste on oikea?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "pääkäyttäjän status" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" "Kertoo että käyttäjä saa tehdä kaiken ilman erikseen annettuja oiekuksia." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "käyttäjänimi" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Vaaditaan. Korkeintaan 30merkkiä. Kirjaimet, numerot ja merkit /./-/_ " "sallittuja" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Anna olemassa oleva käyttäjänimi." -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "aktiivinen" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" "Käyttäjä on aktiivinen. Poista aktiivisuus käyttäjän poistamisen sijaan." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "kuva" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "liittymispvm" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "oletuskieli" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "oletus aikavyöhyke" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "väritä avainsanat" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "sähköpostitunniste" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "uusi sähköpostiosoite" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "oikeudet" @@ -3359,10 +3676,26 @@ msgstr "virheellinen" msgid "Invalid username. Try with a different one." msgstr "Tuntematon käyttäjänimi, yritä uudelleen." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Käyttäjätunnus tai salasana eivät ole oikein." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po index d5717f2b..c1a5dbd5 100644 --- a/taiga/locale/fr/LC_MESSAGES/django.po +++ b/taiga/locale/fr/LC_MESSAGES/django.po @@ -15,15 +15,17 @@ # Nicolas Minelle , 2016 # Nlko , 2015 # Regis TEDONE , 2015 +# Sébastien Talbot , 2016 # Stéphane Mor , 2015 # William Godin , 2015 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-26 14:34+0000\n" -"Last-Translator: Laurent Cabaret \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fr/)\n" "MIME-Version: 1.0\n" @@ -54,32 +56,33 @@ msgid "" msgstr "" "Requis. 255 caractères ou moins. Lettres, chiffres et caractères /./-/_'" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Ce nom d'utilisateur est déjà utilisé." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Cette adresse email est déjà utilisée." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Le jeton ne correspond à aucune invitation." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Cet utilisateur est déjà inscrit." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "L'utilisateur est déjà un membre du projet" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Erreur à la création du nouvel utilisateur." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Jeton invalide" @@ -207,6 +210,15 @@ msgstr "" "Envoyez une image valide. Le fichier que vous avez envoyé n'était pas une " "image ou était une image corrompue." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "Élément bloqué" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" @@ -266,25 +278,25 @@ msgstr "Donnée invalide" msgid "No input provided" msgstr "Aucune entrée fournie" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Impossible de créer un nouvel élément, seuls les éléments existants peuvent " "être mis à jour." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Une liste d'éléments était attendue." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Non trouvé" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Permission refusée" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Erreur du serveur d'application" @@ -359,12 +371,16 @@ msgstr "Erreur d'intégrité ou arguments invalides" msgid "Precondition error" msgstr "Erreur de précondition" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Erreur dans les types de paramètres de filtres" -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' doit être une valeur entière." @@ -512,72 +528,72 @@ msgstr "" " Commentaire : %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Veuillez sélectionner au moins un rôle." -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Fichier de dump obligatoire" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Format de dump invalide" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "Erreur lors de l'importation de données" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "erreur lors de l'importation des listes des attributs de projet" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "" "erreur lors de l'importation des valeurs par défaut des attributs de projet" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "Erreur à l'importation des champs personnalisés" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "Erreur à l'importation des rôles" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "Erreur à l'importation des groupes d'utilisateurs" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "Erreur lors de l'importation des sprints." -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "Erreur à l'importation des pages Wiki" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "Erreur à l'importation des liens Wiki" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "erreur à l'importation des problèmes" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "erreur à l'importation des histoires utilisateur" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "Erreur lors de l'importation des tâches." -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "erreur lors de l'importation des mots-clés" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "erreur lors de l'import des timelines" @@ -596,9 +612,7 @@ msgid "It contain invalid custom fields." msgstr "Contient des champs personnalisés non valides." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Nom dupliqué pour ce projet" @@ -802,6 +816,13 @@ msgid "" "

The Taiga Team

\n" " " msgstr "" +"\n" +"

Importation du Dump projet

\n" +"

Bonjour %(user)s,

\n" +"

Le dump de votre projet a été correctement importé.

\n" +"Allez au %(project)s\n" +"

L'équipe Taiga

" #: taiga/export_import/templates/emails/load_dump-body-text.jinja:1 #, python-format @@ -831,12 +852,12 @@ msgstr "Authentification requise" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "nom" @@ -848,11 +869,11 @@ msgstr "Url de l'icône" msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "description" @@ -863,10 +884,10 @@ msgstr "Url suivante" #: taiga/external_apps/models.py:42 msgid "secret key for ciphering the application tokens" -msgstr "" +msgstr "Clé secrète pour chiffrer le jeton de l'application" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "utilisateur" @@ -874,11 +895,11 @@ msgstr "utilisateur" msgid "application" msgstr "application" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "Nom complet" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "Adresse email" @@ -886,11 +907,11 @@ msgstr "Adresse email" msgid "comment" msgstr "Commentaire" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -961,8 +982,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Le payload n'est pas un json valide" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Le projet n'existe pas" @@ -970,26 +991,26 @@ msgstr "Le projet n'existe pas" msgid "Bad signature" msgstr "Signature non valide" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "L'élément référencé n'existe pas" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "L'état n'existe pas" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Statut changé depuis un commit BitBucket" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Information incorrecte sur le problème" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1000,17 +1021,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Ticket créé depuis BitBucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Ignoré" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1021,7 +1042,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1107,6 +1128,9 @@ msgid "" "\n" "{message}" msgstr "" +"Commentaire depuis GitLab:\n" +"\n" +"{message}" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 #: taiga/permissions/permissions.py:52 @@ -1266,96 +1290,110 @@ msgstr "Administrer les paramètres du projet" msgid "Admin roles" msgstr "Administrer les rôles" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "arguments manquants" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "format de l'image non valide" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Nom de modèle non valide" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Description du modèle non valide" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Au moins un utilisateur doit être un administrateur actif" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "Identifiant utilisateur invalide" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "L'utilisateur n'existe pas" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Vous n'avez pas les permissions pour consulter cet élément" -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Mises à jour partielles non supportées" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "propriétaire" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "projet" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "type du contenu" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "identifiant de l'objet" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "état modifié" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "pièces jointes" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "est obsolète" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "ordre" @@ -1375,18 +1413,34 @@ msgstr "Personnalisé" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Texte" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Texte multi-ligne" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Date" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "Url" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1485,7 +1539,7 @@ msgstr "supprimé" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Non assigné" @@ -1546,23 +1600,23 @@ msgstr "note bloquée" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Vous n'avez pas la permission d'affecter ce sprint à ce problème." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Vous n'avez pas la permission d'affecter cette sévérité à ce problème." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Vous n'avez pas la permission d'affecter cette priorité à ce problème." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Vous n'avez pas la permission d'affecter ce type à ce problème." @@ -1616,27 +1670,27 @@ msgstr "Aimer" msgid "Likes" msgstr "Aime" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "date de démarrage estimée" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "date de fin estimée" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "est fermé" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibilité" @@ -1661,224 +1715,232 @@ msgstr "'{param}' paramètre obligatoire" msgid "'project' parameter is mandatory" msgstr "'project' paramètre obligatoire" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "email" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "Créé le" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "jeton" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "Text supplémentaire de l'invitation" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "classement utilisateur" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "L'utilisateur est déjà un membre du projet" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "Points par défaut" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "statut de l'HU par défaut" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "Etat par défaut des tâches" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "Priorité par défaut" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "Sévérité par défaut" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "statut du problème par défaut" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "type de problème par défaut" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" -msgstr "" +msgstr "logo" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "membres" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "total des jalons" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "total des points d'histoire" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "panneau backlog actif" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "panneau kanban actif" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "panneau wiki actif" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "panneau problèmes actif" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "plateforme de vidéoconférence" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "données complémentaires pour la salle de vidéoconférence" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "Modèle de création" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "Permissions anonymes" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "Permission de l'utilisateur" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "est privé" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "est mis en avant" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "est à la recherche de main d'oeuvre" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "couleurs des tags" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "code bloqué" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "date de mise à jour" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "total" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "fans la semaine dernière" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "fans le mois dernier" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "fans l'année dernière" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "activité de la semaine écoulée" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "activité du mois écoulé" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "activité de l'année écoulée" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "Configurations des modules" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "est archivé" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "couleur" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "limite de travail en cours" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "valeur" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "rôle par défaut du propriétaire" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "options par défaut" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "statuts des us" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "points" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "états des tâches" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "statuts des problèmes" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "types de problèmes" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "priorités" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "sévérités" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "rôles" @@ -1894,29 +1956,29 @@ msgstr "Toutes" msgid "None" msgstr "Aucun" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "date de création" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "entrées dans l'historique" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "notifier les utilisateurs" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Suivre" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "La notification existe pour l'utilisateur et le projet spécifiés" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Valeur non valide pour le niveau de notification" @@ -2282,6 +2344,8 @@ msgid "" "\n" "[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n" msgstr "" +"\n" +"[%(project)s] Récit Utilisateur #%(ref)s \"%(subject)s\" créé\n" #: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4 #, python-format @@ -2429,7 +2493,7 @@ msgstr "La version doit être un nombre entier" #: taiga/projects/occ/mixins.py:59 msgid "The version parameter is not valid" -msgstr "" +msgstr "La version n'est pas valide" #: taiga/projects/occ/mixins.py:75 msgid "The version doesn't match with the current one" @@ -2440,55 +2504,63 @@ msgid "version" msgstr "version" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" msgstr "" -"Vous ne pouvez pas quitter le projet si il n'y a plus d'autres propriétaires" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Adresse email déjà existante" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Rôle non valide pour le projet" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Options par défaut" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Etats de la User Story" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Points" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Etats des tâches" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Statuts des problèmes" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Types de problèmes" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Priorités" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Sévérités" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Rôles" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Sprint futurs" @@ -2496,17 +2568,28 @@ msgstr "Sprint futurs" msgid "Project End" msgstr "Fin du projet" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Jeton invalide" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." -msgstr "" +msgstr "Vous n'avez pas la permission d'affecter ce sprint à cette tâche." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." -msgstr "" +msgstr "Vous n'avez pas la permission d'affecter ce récit à cette tâche." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." -msgstr "" +msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème." #: taiga/projects/tasks/models.py:57 msgid "us order" @@ -2657,6 +2740,236 @@ msgstr "" "\n" "[Taiga] Ajouté au projet '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -2901,15 +3214,17 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Participant" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" +"Vous n'avez pas la permission d'affecter ce sprint à ce récit utilisateur." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" +"Vous n'avez pas la permission d'affecter ce statut à ce récit utilisateur." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -2968,11 +3283,11 @@ msgstr "Votes" msgid "Vote" msgstr "vote" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'content' paramètre obligatoire" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id' paramètre obligatoire" @@ -2984,7 +3299,7 @@ msgstr "dernier modificateur" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "" @@ -2992,66 +3307,66 @@ msgstr "" msgid "Personal info" msgstr "Informations personnelles" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Permissions" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Dates importantes" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Email dupliquée" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Email non valide" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Nom d'utilisateur ou email non valide" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Mail envoyé avec succès!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Jeton invalide" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Paramètre 'mot de passe actuel' requis" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Paramètre 'nouveau mot de passe' requis" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Le mot de passe doit être d'au moins 6 caractères" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Mot de passe actuel incorrect" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Invalide, êtes-vous sûre que le jeton est correct et qu'il n'a pas déjà été " "utilisé ?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Invalide, êtes-vous sûre que le jeton est correct ?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "statut superutilisateur" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3059,25 +3374,25 @@ msgstr "" "Indique que l'utilisateur a toutes les permissions sans avoir à lui les " "donner explicitement" -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "nom d'utilisateur" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Obligatoire. 30 caractères maximum. Lettres, nombres et les caractères /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Entrez un nom d'utilisateur valide" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "actif" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3085,43 +3400,59 @@ msgstr "" "Indique qu'un utilisateur est considéré ou non comme actif. Désélectionnez " "cette option au lieu de supprimer le compte utilisateur." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biographie" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "photo" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "date d'inscription" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "langage par défaut" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "thème par défaut" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "Fuseau horaire par défaut" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "changer la couleur des tags" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "jeton email" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "nouvelle adresse email" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "permissions" @@ -3133,10 +3464,26 @@ msgstr "invalide" msgid "Invalid username. Try with a different one." msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Aucun utilisateur avec ce nom ou ce mot de passe." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po index c41f6400..2f042498 100644 --- a/taiga/locale/it/LC_MESSAGES/django.po +++ b/taiga/locale/it/LC_MESSAGES/django.po @@ -14,9 +14,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 13:03+0000\n" -"Last-Translator: F B \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/it/)\n" "MIME-Version: 1.0\n" @@ -48,32 +49,33 @@ msgstr "" "Sono richiesti 255 caratteri, o meno, contenenti: lettere, numeri e " "caratteri /./-/_ " -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Il nome utente appena scelto è già utilizzato." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "L'email inserita è già utilizzata." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Il token non corrisponde a nessun invito valido" -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "L'Utente è già registrato." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "Questo utente fa già parte del progetto." -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Errore nella creazione dell'utente." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Token non valido" @@ -193,6 +195,15 @@ msgstr "" "Carica un'immagina valida. Il file caricato potrebbe non essere un'immagine " "o l'immagine potrebbe essere corrotta. " +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "La pagina non è 'last', né può essere convertita come int." @@ -250,25 +261,25 @@ msgstr "Dati non validi" msgid "No input provided" msgstr "Non è stato fornito nessun input" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Non è possibile creare un nuovo elemento, solo quelli esistenti possono " "essere aggiornati" -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Ci si aspetta una lista di oggetti." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Non trovato" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Permesso negato" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Errore sul server" @@ -344,12 +355,16 @@ msgstr "Errore di integrità causato da un argomento invalido o sbagliato" msgid "Precondition error" msgstr "Errore di precondizione" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Errore nel filtro del tipo di parametri." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'Progetto' deve essere un valore intero." @@ -503,72 +518,72 @@ msgstr "" "\n" "Commento: %(comment)s" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "C'è bisogno di almeno un ruolo" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "E' richiesto un file di dump" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Formato di dump invalido" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "Errore nell'importazione del progetto dati" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "Errore nell'importazione della lista degli attributi di progetto" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "" "Errore nell'importazione dei valori predefiniti degli attributi del progetto." -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "Errore nell'importazione degli attributi personalizzati" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "Errore nell'importazione i ruoli" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "Errore nell'importazione delle iscrizioni" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "errore nell'importazione degli sprints" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "Errore nell'importazione delle pagine wiki" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "Errore nell'importazione dei link di wiki" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "errore nell'importazione dei problemi" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "Errore nell'importazione delle user story" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "Errore nell'importazione dei compiti " -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "Errore nell'importazione dei tags" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "Errore nell'importazione delle timelines" @@ -587,9 +602,7 @@ msgid "It contain invalid custom fields." msgstr "Contiene campi personalizzati invalidi." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Il nome del progetto è duplicato" @@ -902,12 +915,12 @@ msgstr "E' richiesta l'autenticazione" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "nome" @@ -919,11 +932,11 @@ msgstr "Url dell'icona" msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "descrizione" @@ -937,7 +950,7 @@ msgid "secret key for ciphering the application tokens" msgstr "chiave segreta per cifrare i token dell'applicazione" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "utente" @@ -945,11 +958,11 @@ msgstr "utente" msgid "application" msgstr "applicazione" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "Nome completo" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "Inserisci un indirizzo e-mail valido." @@ -957,11 +970,11 @@ msgstr "Inserisci un indirizzo e-mail valido." msgid "comment" msgstr "Commento" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -1038,8 +1051,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Il carico non è un json valido" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Il progetto non esiste" @@ -1047,26 +1060,26 @@ msgstr "Il progetto non esiste" msgid "Bad signature" msgstr "Firma non valida" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "L'elemento di riferimento non esiste" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Lo stato non esiste" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Lo stato è stato modificato a seguito di un commit di BitBucket" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Informazione sul problema non valida" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1086,17 +1099,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Problema creato da BItBucket" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Commento sul problema non valido" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1116,7 +1129,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1398,96 +1411,110 @@ msgstr "Valori dell'amministratore del progetto" msgid "Admin roles" msgstr "Ruoli dell'amministratore" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Argomento non valido" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Formato dell'immagine non valido" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Il nome del template non è valido" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "La descrizione del template non è valida" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Almeno uno degli utenti deve essere attivo come amministratore" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Non hai il permesso di vedere questo elemento." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Aggiornamento non parziale non supportato" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "L'ID di progetto non corrisponde tra oggetto e progetto" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "proprietario" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "progetto" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "tipo di contenuto" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "ID dell'oggetto" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "data modificata" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "file allegato" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "non approvato" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "ordine" @@ -1507,18 +1534,34 @@ msgstr "Personalizzato" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Testo" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Testo multi-linea" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Data" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1617,7 +1660,7 @@ msgstr "rimosso" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Non assegnato" @@ -1678,23 +1721,23 @@ msgstr "nota bloccata" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Non hai i permessi per aggiungere questo sprint a questo problema" -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Non hai i permessi per aggiungere questo stato a questo problema" -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Non hai i permessi per aggiungere questa criticità a questo problema" -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Non hai i permessi per aggiungere questa priorità a questo problema." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Non hai i permessi per aggiungere questa tipologia a questo problema" @@ -1748,27 +1791,27 @@ msgstr "Like" msgid "Likes" msgstr "Piaciuto" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "lumaca" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "data stimata di inizio" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "data stimata di fine" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "è concluso" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibilità" @@ -1794,224 +1837,232 @@ msgstr "il parametro '{param}' è obbligatorio" msgid "'project' parameter is mandatory" msgstr "il parametro 'project' è obbligatorio" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "email" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "creato a " -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "testo ulteriore per l'invito" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "ordine dell'utente" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "L'utente è già membro del progetto" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "punti predefiniti" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "stati predefiniti per le storie utente" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "stati predefiniti del compito" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "priorità predefinita" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "criticità predefinita" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "stato predefinito del problema" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "tipologia predefinita del problema" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "membri" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "tappe totali" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "punti totali della storia" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "pannello di backlog attivo" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "pannello kanban attivo" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "pannello wiki attivo" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "pannello dei problemi attivo" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "sistema di videoconferenza" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "ulteriori dati di videoconferenza " -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "creazione del template" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "permessi anonimi" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "permessi dell'utente" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "è privato" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "in vetrina" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "sta cercando persone" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "note sulla ricerca delle persone " -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "colori dei tag" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "tempo e data aggiornati" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "conta" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "fans nella settimana" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "fans nel mese" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "fans nell'anno" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "attività nella settimana" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "attività nel mese" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "attività nell'anno" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "configurazione dei moduli" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "è archivitato" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "colore" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "limite dei lavori in corso" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "valore" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "ruolo proprietario predefinito" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "opzioni predefinite " -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "stati della storia utente" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "punti" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "stati del compito" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "stati del probema" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "tipologie del problema" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "priorità" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "criticità " -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "ruoli" @@ -2027,29 +2078,29 @@ msgstr "Tutti" msgid "None" msgstr "Nessuno" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "tempo e data creati" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "inserimenti della storia" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "notifica utenti" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Osservato" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "La notifica esiste per l'utente e il progetto specificati" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Valore non valido per il livello di notifica" @@ -2931,54 +2982,63 @@ msgid "version" msgstr "versione" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Non puoi abbandonare il progetto se non ci sono altri proprietari" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "L'indirizzo email è già usato" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Ruolo di progetto non valido" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Opzioni predefinite" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Stati della storia utente" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Punti" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Stati del compito" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Stati del problema" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Tipologie del problema" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Priorità" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Criticità" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Ruoli" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Sprint futuri" @@ -2986,16 +3046,27 @@ msgstr "Sprint futuri" msgid "Project End" msgstr "Termine di progetto" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Token non valido" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "Non hai i permessi per aggiungere questo sprint a questo compito." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" "Non hai i permessi per aggiungere questa storia utente a questo compito." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "Non hai i permessi per aggiungere questo stato a questo compito." @@ -3182,6 +3253,236 @@ msgstr "" "\n" "[Taiga] aggiunto al progetto '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3425,16 +3726,16 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Non hai i permessi per aggiungere questo sprint a questa storia utente." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "Non hai i permessi per aggiungere questo stato a questa storia utente." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Stiamo generando la storia utente #{ref} - {subject}" @@ -3493,11 +3794,11 @@ msgstr "Voti" msgid "Vote" msgstr "Voto" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "il parametro 'contenuto' è obbligatorio" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "Il parametro 'ID progetto' è obbligatorio" @@ -3509,7 +3810,7 @@ msgstr "ultima modificatore" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Controlla le API della storie per la differenza esatta" @@ -3517,66 +3818,66 @@ msgstr "Controlla le API della storie per la differenza esatta" msgid "Personal info" msgstr "Informazioni personali" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Permessi" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Date importanti" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "E-mail duplicata" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "E-mail non valida" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Username o e-mail non validi" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Mail inviata con successo!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Token non valido" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "E' necessario il parametro della password corrente" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "E' necessario il parametro della nuovo password" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Lunghezza della password non valida, sono necessari almeno 6 caratteri" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Password corrente non valida" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Non valido. Sei sicuro che il token sia corretto e che tu non l'abbia già " "usato in precedenza?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Non valido. Sicuro che il token sia corretto?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "Stato del super-utente" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3584,26 +3885,26 @@ msgstr "" "Definisce che questo utente ha tutti i permessi senza assegnarglieli " "esplicitamente." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "nome utente" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Richiede 30 caratteri o meno. Deve comprendere: lettere, numeri e caratteri " "come /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Inserisci un nome utente valido." -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "attivo" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3611,43 +3912,59 @@ msgstr "" "Definisce se questo utente debba essere trattato come attivo. Deseleziona " "questo invece di eliminare gli account." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "fotografia" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "data di inizio partecipazione" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "lingua predefinita" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "tema predefinito" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "timezone predefinita" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "colora i tag" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "token e-mail" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "nuovo indirizzo e-mail" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "permessi" @@ -3659,10 +3976,26 @@ msgstr "non valido" msgid "Invalid username. Try with a different one." msgstr "Nome utente non valido. Provane uno diverso." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Il nome utente o la password non corrispondono all'utente." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po index 7e499bcb..c6cc1c9e 100644 --- a/taiga/locale/nl/LC_MESSAGES/django.po +++ b/taiga/locale/nl/LC_MESSAGES/django.po @@ -9,9 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/nl/)\n" "MIME-Version: 1.0\n" @@ -41,32 +42,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Verplicht. 255 tekens of minder. Letters, nummers en /./-/_ tekens'" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Gebruikersnaame is al in gebruik." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "E-mail adres is al in gebruik." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Token stemt niet overeen met een geldige uitnodiging." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Gebruiker is al geregistreerd." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Fout bij het aanmaken van een nieuwe gebruiker." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Ongeldig token" @@ -193,6 +195,15 @@ msgstr "" "Upload een geldige afbeelding. Het bestand dat je hebt geuploadet was ofwel " "een afbeelding ofwel een corrupte afbeelding." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Pagina is niet 'last', noch kan het omgezet worden naar een int." @@ -250,24 +261,24 @@ msgstr "Ongeldige data" msgid "No input provided" msgstr "Geen input gegeven" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Kan geen nieuw item aanmaken, enkel bestaande items mogen bijgewerkt worden." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Verwachtte een lijst van items." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Niet gevonden" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Toestemming geweigerd" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Server applicatie fout" @@ -342,12 +353,16 @@ msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten" msgid "Precondition error" msgstr "Preconditie fout" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Fout in filter params types." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' moet een integer waarde zijn." @@ -492,71 +507,71 @@ msgstr "" " Commentaar: %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "We hadden minstens één rol nodig" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Dump file nodig" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Ongeldig dump formaat" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "fout bij het importeren van project data" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "fout bij importeren van project attributenlijst" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "fout bij importeren van standaard projectattributen waarden" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "fout bij importeren eigen attributen" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "fout bij importeren rollen" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "fout bij importeren lidmaatschappen" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "fout bij importeren sprints" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "fout bij importeren wiki pagina's" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "fout bij importeren wiki links" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "fout bij importeren issues" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "fout bij importeren user stories" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "fout bij importeren taken" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "fout bij importeren tags" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "fout bij importeren tijdlijnen" @@ -575,9 +590,7 @@ msgid "It contain invalid custom fields." msgstr "Het bevat ongeldige eigen velden:" #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Naam gedupliceerd voor het project" @@ -765,12 +778,12 @@ msgstr "" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "naam" @@ -782,11 +795,11 @@ msgstr "" msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "omschrijving" @@ -800,7 +813,7 @@ msgid "secret key for ciphering the application tokens" msgstr "" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "" @@ -808,11 +821,11 @@ msgstr "" msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "volledige naam" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "e-mail adres" @@ -820,11 +833,11 @@ msgstr "e-mail adres" msgid "comment" msgstr "commentaar" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -896,8 +909,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "De payload is geen geldige json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Het project bestaat niet" @@ -905,26 +918,26 @@ msgstr "Het project bestaat niet" msgid "Bad signature" msgstr "Slechte signature" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Het element waarnaar verwezen wordt bestaat niet" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "De status bestaat niet" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Status veranderd door Bitbucket commit" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Ongeldige issue informatie" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -935,17 +948,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Ongeldige issue commentaar informatie" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -956,7 +969,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1198,96 +1211,110 @@ msgstr "Admin project waarden" msgid "Admin roles" msgstr "Admin rollen" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Onvolledige argumenten" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Ongeldig afbeelding formaat" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Ongeldige template naam" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Ongeldige template omschrijving" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Minstens één van de gebruikers moet een active admin zijn" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Je hebt geen toestamming om dat te bekijken." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "Project ID van object is niet gelijk aan die van het project" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "eigenaar" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "project" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "inhoud type" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "object id" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "gemodifieerde datum" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "bijgevoegd bestand" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "is verouderd" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "volgorde" @@ -1307,18 +1334,34 @@ msgstr "" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 -msgid "Text" +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" msgstr "" #: taiga/projects/custom_attributes/choices.py:27 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1417,7 +1460,7 @@ msgstr "verwijderd" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Niet toegewezen" @@ -1478,25 +1521,25 @@ msgstr "geblokkeerde notitie" msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Je hebt geen toestemming om deze sprint op deze issue te zetten." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Je hebt geen toestemming om deze status toe te kennen aan dze issue." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "" "Je hebt geen toestemming om dit ernstniveau toe te kennen aan deze issue." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "" "Je hebt geen toestemming om deze prioriteit toe te kennen aan deze issue." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Je hebt geen toestemming om dit type toe te kennen aan deze issue." @@ -1550,27 +1593,27 @@ msgstr "Vind ik leuk" msgid "Likes" msgstr "Personen die dit leuk vinden" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "geschatte start datum" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "geschatte datum van afwerking" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "is gesloten" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "beschikbaarheid" @@ -1595,224 +1638,232 @@ msgstr "'{param}' parameter is verplicht" msgid "'project' parameter is mandatory" msgstr "'project' parameter is verplicht" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "aangemaakt op" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "uitnodiging extra text" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "gebruiker volgorde" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "The gebruikers is al lid van het project" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "standaard punten" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "standaard US status" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "default taak status" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "standaard prioriteit" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "standaard ernstniveau" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "standaard issue status" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "standaard issue type" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "leden" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "totaal van de milestones" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "totaal story points" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "actief backlog paneel" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "actief kanban paneel" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "actief wiki paneel" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "actief issues paneel" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "videoconference systeem" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "aanmaak template" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "anonieme toestemmingen" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "gebruikers toestemmingen" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "is privé" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "tag kleuren" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "gewijzigde datum en tijd" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "module config" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "is gearchiveerd" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "kleur" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "work in progress limiet" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "waarde" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "standaard rol eigenaar" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "standaard instellingen" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "us statussen" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "punten" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "taak statussen" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "issue statussen" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "issue types" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "prioriteiten" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "ernstniveaus" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "rollen" @@ -1828,29 +1879,29 @@ msgstr "" msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "aanmaak datum en tijd" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "geschiedenis items" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "verwittig gebruikers" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Verwittiging bestaat voor gespecifieerde gebruiker en project" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "" @@ -2376,54 +2427,63 @@ msgid "version" msgstr "versie" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Je kan het project niet verlaten als er geen andere eigenaars zijn" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "E-mail adres is al in gebruik" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Ongeldige rol voor project" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Standaard opties" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Status van User story" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Punten" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Statussen van taken" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Statussen van Issues" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Types van issue" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Prioriteiten" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Ernstniveaus" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Rollen" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Toekomstige sprint" @@ -2431,15 +2491,26 @@ msgstr "Toekomstige sprint" msgid "Project End" msgstr "Project einde" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Token is ongeldig" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2580,6 +2651,236 @@ msgstr "" "\n" "[Taiga] Toegevoegd aan het project '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -2823,15 +3124,15 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -2890,11 +3191,11 @@ msgstr "Stemmen" msgid "Vote" msgstr "Stem" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'inhoud' parameter is verplicht" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parameter is verplicht" @@ -2906,7 +3207,7 @@ msgstr "gebruiker met laatste wijziging" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "" @@ -2914,64 +3215,64 @@ msgstr "" msgid "Personal info" msgstr "Persoonlijke info" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Toestemmingen" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Belangrijke data" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Gedupliceerde e-mail" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Ongeldige e-mail" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Ongeldige gebruikersnaam of e-mail" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Mail met succes verzonden!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Token is ongeldig" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Huidig wachtwoord parameter vereist" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Nieuw wachtwoord parameter vereist" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Ongeldige lengte van wachtwoord, minstens 6 tekens vereist" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Ongeldig huidig wachtwoord" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "Ongeldig, weet je zeker dat het token correct en ongebruikt is?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Ongeldig, weet je zeker dat het token correct is?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "superuser status" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -2979,24 +3280,24 @@ msgstr "" "Beduidt dat deze gebruik alle toestemmingen heeft zonder deze expliciet toe " "te wijzen." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "gebruikersnaam" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Vereist. 30 of minder karakters. Letters, nummers en /./-/_ karakters" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Geef een geldige gebruikersnaam in" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "actief" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3004,43 +3305,59 @@ msgstr "" "Beduidt of deze gebruiker als actief moet behandeld worden. Deselecteer dit " "i.p.v. accounts te verwijderen." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografie" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "foto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "toetrededatum" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "standaard taal" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "standaard tijdzone" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "kleur tags" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "e-mail token" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "nieuw e-mail adres" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "toestemmingen" @@ -3052,10 +3369,26 @@ msgstr "ongeldig" msgid "Invalid username. Try with a different one." msgstr "Ongeldige gebruikersnaam. Probeer met een andere." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po index 0d5a6368..69cce377 100644 --- a/taiga/locale/pl/LC_MESSAGES/django.po +++ b/taiga/locale/pl/LC_MESSAGES/django.po @@ -10,9 +10,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/pl/)\n" "MIME-Version: 1.0\n" @@ -43,32 +44,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Wymagane. Maksymalnie 255 znaków. Litery, cyfry oraz /./-/_ " -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Nazwa użytkownika jest już używana." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Ten adres email jest już w użyciu." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Token nie zgadza się z żadnym zaproszeniem" -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Użytkownik już zarejestrowany" -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Błąd przy tworzeniu użytkownika." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Nieprawidłowy token" @@ -187,6 +189,15 @@ msgstr "" "Prześlij właściwy obraz. Plik który próbujesz przesłać nie jest obrazem lub " "jest uszkodzony." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Strona nie jest ostatnią i nie może zostać zmieniona na int." @@ -244,25 +255,25 @@ msgstr "Nieprawidłowa dana" msgid "No input provided" msgstr "Nic nie wpisano" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Nie można utworzyć nowego obiektu, tylko istniejące obiekty mogą być " "aktualizowane." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Oczekiwana lista elementów." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Nie znaleziono" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Dostęp zabroniony" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Błąd aplikacji serwera" @@ -337,12 +348,16 @@ msgstr "Błąd integralności dla błędnych lub nieprawidłowych argumentów" msgid "Precondition error" msgstr "Błąd warunków wstępnych" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Błąd w parametrach typów filtrów." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' musi być wartością typu int." @@ -493,71 +508,71 @@ msgstr "" " Komentarz: %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Potrzeba conajmiej jednej roli" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Wymagany plik zrzutu" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Nieprawidłowy format zrzutu" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "błąd w trakcie importu danych projektu" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "błąd w trakcie importu atrybutów projektu" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "błąd w trakcie importu domyślnych atrybutów projektu" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "błąd w trakcie importu niestandardowych atrybutów" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "błąd w trakcie importu ról" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "błąd w trakcie importu członkostw" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "błąd w trakcie importu sprintów" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "błąd w trakcie importu stron Wiki" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "błąd w trakcie importu linków Wiki" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "błąd w trakcie importu zgłoszeń" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "błąd w trakcie importu historyjek użytkownika" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "błąd w trakcie importu zadań" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "błąd w trakcie importu tagów" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "błąd w trakcie importu osi czasu" @@ -576,9 +591,7 @@ msgid "It contain invalid custom fields." msgstr "Zawiera niewłaściwe pola niestandardowe." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Nazwa projektu zduplikowana" @@ -833,12 +846,12 @@ msgstr "" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "nazwa" @@ -850,11 +863,11 @@ msgstr "" msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "opis" @@ -868,7 +881,7 @@ msgid "secret key for ciphering the application tokens" msgstr "" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "użytkownik" @@ -876,11 +889,11 @@ msgstr "użytkownik" msgid "application" msgstr "aplikacja" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "Imię i Nazwisko" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "adres e-mail" @@ -888,11 +901,11 @@ msgstr "adres e-mail" msgid "comment" msgstr "komentarz" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -964,8 +977,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Źródło nie jest prawidłowym plikiem json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Projekt nie istnieje" @@ -973,26 +986,26 @@ msgstr "Projekt nie istnieje" msgid "Bad signature" msgstr "Błędna sygnatura" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Element referencyjny nie istnieje" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Status nie istnieje" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Status zmieniony przez commit z BitBucket" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Nieprawidłowa informacja o zgłoszeniu" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1009,17 +1022,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Zgłoszenie utworzone przez BitBucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Nieprawidłowa informacja o komentarzu do zgłoszenia" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1036,7 +1049,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1306,96 +1319,110 @@ msgstr "Administruj wartościami projektu" msgid "Admin roles" msgstr "Administruj rolami" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Pola niekompletne" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Niepoprawny format obrazka" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Nieprawidłowa nazwa szablonu" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Nieprawidłowy opis szablonu" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Przynajmniej jeden użytkownik musi być aktywnym Administratorem" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Nie masz uprawnień by to zobaczyć." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "ID nie pasuje pomiędzy obiektem a projektem" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "właściciel" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "projekt" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "typ zawartości" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "id obiektu" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "data modyfikacji" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "załączony plik" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "jest przestarzałe" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "kolejność" @@ -1415,18 +1442,34 @@ msgstr "Niestandardowy" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Tekst" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Teks wielowierszowy" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1525,7 +1568,7 @@ msgstr "usuniete" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Nieprzypisane" @@ -1586,23 +1629,23 @@ msgstr "zaglokowana notatka" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Nie masz uprawnień do połączenia tego zgłoszenia ze sprintem." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Nie masz uprawnień do ustawienia statusu dla tego zgłoszenia." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Nie masz uprawnień do ustawienia ważności dla tego zgłoszenia." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Nie masz uprawnień do ustawienia priorytetu dla tego zgłoszenia." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Nie masz uprawnień do ustawienia typu dla tego zgłoszenia." @@ -1656,27 +1699,27 @@ msgstr "" msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "szacowana data rozpoczecia" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "szacowana data zakończenia" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "jest zamknięte" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "dostępność" @@ -1701,224 +1744,232 @@ msgstr "'{param}' parametr jest obowiązkowy" msgid "'project' parameter is mandatory" msgstr "'project' parametr jest obowiązkowy" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "utwórz na" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "dodatkowy tekst w zaproszeniu" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "kolejność użytkowników" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "Użytkownik już jest członkiem tego projektu" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "domyślne punkty" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "domyślny status dla HU" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "domyślny status dla zadania" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "domyślny priorytet" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "domyślna ważność" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "domyślny status dla zgłoszenia" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "domyślny typ dla zgłoszenia" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "członkowie" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "wszystkich kamieni milowych" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "wszystkich punktów " -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "aktywny panel backlog" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "aktywny panel Kanban" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "aktywny panel Wiki" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "aktywny panel zgłoszeń " -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "system wideokonferencji" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "dodatkowe dane dla wideokonferencji" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "szablon " -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "uprawnienia anonimowych" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "uprawnienia użytkownika" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "jest prywatna" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "kolory tagów" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "data aktualizacji" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "ilość" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "konfiguracja modułów" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "zarchiwizowane" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "kolor" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "limit postępu prac" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "wartość" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "domyśla rola właściciela" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "domyślne opcje" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "statusy HU" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "pinkty" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "statusy zadań" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "statusy zgłoszeń" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "typy zgłoszeń" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "priorytety" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "ważność" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "role" @@ -1934,29 +1985,29 @@ msgstr "" msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "data utworzenia" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "wpisy historii" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "powiadom użytkowników" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Obserwowane" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Powiadomienie istnieje dla określonego użytkownika i projektu" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Nieprawidłowa wartość dla poziomu notyfikacji" @@ -2713,54 +2764,63 @@ msgid "version" msgstr "wersja" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Nie możesz opuścić projektu, jeśli jesteś jego jedynym właścicielem" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Tena adres e-mail jest już w użyciu" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Nieprawidłowa rola w projekcie" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Domyślne opcje" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Statusy historyjek użytkownika" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Punkty" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Statusy zadań" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Statusy zgłoszeń" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Typu zgłoszeń" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Priorytety" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Ważność" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Role" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Przyszły sprint" @@ -2768,16 +2828,27 @@ msgstr "Przyszły sprint" msgid "Project End" msgstr "Zakończenie projektu" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Nieprawidłowy token." + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "Nie masz uprawnień do ustawiania sprintu dla tego zadania." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" "Nie masz uprawnień do ustawiania historyjki użytkownika dla tego zadania" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania" @@ -2946,6 +3017,236 @@ msgstr "" "\n" "[Taiga] Dodany do projektu '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3188,17 +3489,17 @@ msgstr "Właściciel produktu" msgid "Stakeholder" msgstr "Interesariusz" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Nie masz uprawnień do ustawiania sprintu dla tej historyjki użytkownika." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" "Nie masz uprawnień do ustawiania statusu do tej historyjki użytkownika." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -3257,11 +3558,11 @@ msgstr "Głosy" msgid "Vote" msgstr "Głos" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "Parametr 'zawartość' jest wymagany" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "Parametr 'id_projektu' jest wymagany" @@ -3273,7 +3574,7 @@ msgstr "ostatnio zmodyfikowane przez" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Dla pełengo diffa sprawdź API historii" @@ -3281,68 +3582,68 @@ msgstr "Dla pełengo diffa sprawdź API historii" msgid "Personal info" msgstr "Informacje osobiste" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Uprawnienia" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Ważne daty" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Zduplikowany adres e-mail" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Niepoprawny adres e-mail" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Nieprawidłowa nazwa użytkownika lub adrs e-mail" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "E-mail wysłany poprawnie!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Nieprawidłowy token." - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Należy podać bieżące hasło" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Należy podać nowe hasło" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "" "Nieprawidłowa długość hasła - wymagane jest co najmniej 6 znaków" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Podałeś nieprawidłowe bieżące hasło" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Niepoprawne, jesteś pewien, że token jest poprawny i nie używałeś go " "wcześniej? " -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Niepoprawne, jesteś pewien, że token jest poprawny?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "status SUPERUSER" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3350,24 +3651,24 @@ msgstr "" "Oznacza, że ten użytkownik posiada wszystkie uprawnienia bez konieczności " "ich przydzielania." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "nazwa użytkownika" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Wymagane. 30 znaków. Liter, cyfr i znaków /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Wprowadź poprawną nazwę użytkownika" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "aktywny" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3375,43 +3676,59 @@ msgstr "" "Oznacza, że ten użytkownik ma być traktowany jako aktywny. Możesz to " "odznaczyć zamiast usuwać konto." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "zdjęcie" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "data dołączenia" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "domyślny język Taiga" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "domyślny szablon Taiga" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "domyśla strefa czasowa" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "kolory tagów" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "tokem e-mail" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "nowy adres e-mail" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "uprawnienia" @@ -3423,10 +3740,26 @@ msgstr "Niepoprawne" msgid "Invalid username. Try with a different one." msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Nazwa użytkownika lub hasło są nieprawidłowe" +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po index 9e3d6598..7c9c5270 100644 --- a/taiga/locale/pt_BR/LC_MESSAGES/django.po +++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po @@ -19,9 +19,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/pt_BR/)\n" "MIME-Version: 1.0\n" @@ -51,32 +52,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Obrigatório. No máximo 255 caracteres. Letras, números e /./-/_ ." -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Nome de usuário já está em uso." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Este e-mail já está em uso." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Esse token não bate com nenhum convite." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Este usuário já está registrado." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "O usuário já é membro do projeto." -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Erro ao criar um novo usuário." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Token inválido" @@ -194,6 +196,15 @@ msgstr "" "Envie uma imagem válida. O arquivo que você mandou ou não era uma imagem ou " "está corrompido." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Página não é \"última\", nem pode ser convertída para um inteiro." @@ -251,25 +262,25 @@ msgstr "Dados inválidos" msgid "No input provided" msgstr "Nenhuma entrada providenciada" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Não é possível criar um novo item, somente itens já existentes podem ser " "atualizados." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Esperada uma lista de itens." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Não encontrado" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Permissão negada" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Erro no servidor da aplicação" @@ -344,12 +355,16 @@ msgstr "Erro de Integridade para argumentos inválidos ou errados" msgid "Precondition error" msgstr "Erro de pré-condição" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Erro nos tipos de parâmetros do filtro." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'projeto' deve ser um valor inteiro." @@ -501,71 +516,71 @@ msgstr "" " Comentário: %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Nós precisamos de pelo menos uma função" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Necessário de arquivo de restauração" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Formato de aquivo de restauração inválido" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "erro ao importar informações de projeto" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "erro importando lista de atributos do projeto" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "erro importando valores de atributos do projeto padrão" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "erro importando atributos personalizados" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "erro importando funcões" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "erro importando filiações" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "erro importando sprints" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "erro importando páginas wiki" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "erro importando wiki links" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "erro importando casos" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "erro importando user stories" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "erro importando tarefas" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "erro importando tags" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "erro importando linha do tempo" @@ -584,9 +599,7 @@ msgid "It contain invalid custom fields." msgstr "Contém campos personalizados inválidos" #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Nome duplicado para o projeto" @@ -840,12 +853,12 @@ msgstr "Autenticação necessária" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "Nome" @@ -857,11 +870,11 @@ msgstr "Ícone da url" msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "descrição" @@ -875,7 +888,7 @@ msgid "secret key for ciphering the application tokens" msgstr "chave secreta para cifrar os tokens da aplicação" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "usuário" @@ -883,11 +896,11 @@ msgstr "usuário" msgid "application" msgstr "aplicação" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "nome completo" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "endereço de e-mail" @@ -895,11 +908,11 @@ msgstr "endereço de e-mail" msgid "comment" msgstr "comentário" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -971,8 +984,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "A carga não é um json válido" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "O projeto não existe" @@ -980,26 +993,26 @@ msgstr "O projeto não existe" msgid "Bad signature" msgstr "Assinatura Ruim" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "O elemento referenciado não existe" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "O estatus não existe" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Status alterado em Bitbucket commit" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Informação de caso inválida" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1016,17 +1029,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Caso criado pelo Bitbucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Informação de comentário de caso inválido" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1043,7 +1056,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1312,96 +1325,110 @@ msgstr "Valores projeto admin" msgid "Admin roles" msgstr "Funções Admin" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Argumentos incompletos" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Formato de imagem inválida" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Nome de template inválido" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Descrição de template inválida" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Pelo menos one dos usuários deve ser um administrador ativo" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Você não tem permissão para ver isso" -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Atualizações parciais não são suportadas" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "ID do projeto não combina entre objeto e projeto" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "dono" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "projeto" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "tipo de conteúdo" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "identidade de objeto" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "data modificação" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "arquivo anexado" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "está obsoleto" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "ordem" @@ -1421,18 +1448,34 @@ msgstr "Personalizado" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Texto" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Multi-linha" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Data" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1531,7 +1574,7 @@ msgstr "removido" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Não-atribuído" @@ -1592,23 +1635,23 @@ msgstr "nota bloqueada" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Você não tem permissão para colocar esse sprint para esse caso." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Você não tem permissão para colocar esse status para esse caso." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Você não tem permissão para colocar essa severidade para esse caso." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Você não tem permissão para colocar essa prioridade para esse caso." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Você não tem permissão para colocar esse tipo para esse caso." @@ -1662,27 +1705,27 @@ msgstr "Curtir" msgid "Likes" msgstr "Curtidas" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slug" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "data de início estimada" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "data de encerramento estimada" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "está fechado" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibilidade" @@ -1707,224 +1750,232 @@ msgstr "'{param}' parametro é mandatório" msgid "'project' parameter is mandatory" msgstr "'project' parametro é mandatório" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "email" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "criado em" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "token" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "texto extra de convite" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "ordem de usuário" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "O usuário já é membro do projeto" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "pontos padrão" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "status de US padrão" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "status padrão de tarefa" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "prioridade padrão" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "severidade padrão" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "status padrão de caso" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "tipo padrão de caso" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "membros" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "total de marcos de progresso" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "pontos totais de US" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "painel de backlog ativo" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "painel de kanban ativo" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "painel de wiki ativo" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "painel de casos ativo" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "sistema de vídeo conferência" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "informação extra de vídeo conferência" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "template de criação" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "permissão anônima" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "permissão de usuário" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "é privado" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "cores de tags" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "data de atualização" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "contagem" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "configurações de módulos" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "está arquivado" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "cor" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "trabalho no limite de progresso" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "valor" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "função padrão para dono " -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "opções padrão" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "status de US" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "pontos" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "status de tarefa" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "status de casos" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "tipos de caso" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "severidades" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "funções" @@ -1940,29 +1991,29 @@ msgstr "" msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "data de criação" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "histórico de entradas" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "notificar usuário" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Observado" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Existe notificação para usuário e projeto especifcado" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Valor inválido para nível de notificação" @@ -2697,54 +2748,63 @@ msgid "version" msgstr "versão" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Você não pode deixar o projeto se não há mais donos" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Endereço de e-mail já utilizado" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Função inválida para projeto" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Opções padrão" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Status de user story" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Pontos" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Status de tarefas" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Status de casos" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Tipos de casos" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Prioridades" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Severidades" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Funções" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Sprint futuro" @@ -2752,15 +2812,26 @@ msgstr "Sprint futuro" msgid "Project End" msgstr "Fim do projeto" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Token é inválido" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "Você não tem permissão para colocar esse sprint para essa tarefa." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "Você não tem permissão para colocar essa user story para essa tarefa." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "Você não tem permissão para colocar esse status para essa tarefa." @@ -2929,6 +3000,236 @@ msgstr "" "\n" "[Taiga] Adicionado ao projeto '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3171,15 +3472,15 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "Você não tem permissão para colocar esse sprint para essa user story." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "Você não tem permissão para colocar esse status para essa user story." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -3238,11 +3539,11 @@ msgstr "Votos" msgid "Vote" msgstr "Vote" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "parâmetro 'conteúdo' é mandatório" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "parametro 'project_id' é mandatório" @@ -3254,7 +3555,7 @@ msgstr "último modificador" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Verifique o histórico da API para a exata diferença" @@ -3262,66 +3563,66 @@ msgstr "Verifique o histórico da API para a exata diferença" msgid "Personal info" msgstr "Informação pessoal" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Permissões" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Datas importantes" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "E-mail duplicado" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Não é um e-mail válido" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Usuário ou e-mail inválido" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "E-mail enviado com sucesso" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Token é inválido" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Parâmetro de senha atual necessário" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Parâmetro de nova senha necessário" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Comprimento de senha inválido, pelo menos 6 caracteres necessários" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Senha atual inválida" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Inválido, você está certo que o token está correto e não foi usado " "anteriormente?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Inválido, tem certeza que o token está correto?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "status de superuser" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3329,24 +3630,24 @@ msgstr "" "Designa que esse usuário tem todas as permissões sem explicitamente assiná-" "las" -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "usuário" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Requerido. 30 caracteres ou menos. Letras, números e caracteres /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Digite um usuário válido" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "ativo" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3354,43 +3655,59 @@ msgstr "" "Designa quando esse usuário deve ser tratado como ativo. desmarque isso em " "vez de deletar contas." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "foto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "data ingressado" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "lingua padrão" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "tema padrão" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "fuso horário padrão" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "tags coloridas" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "token de e-mail" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "novo endereço de email" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "permissões" @@ -3402,10 +3719,26 @@ msgstr "inválido" msgid "Invalid username. Try with a different one." msgstr "Usuário inválido. Tente com um diferente." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Usuário ou senha não correspondem ao usuário" +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po index c38ba995..512d232e 100644 --- a/taiga/locale/ru/LC_MESSAGES/django.po +++ b/taiga/locale/ru/LC_MESSAGES/django.po @@ -9,14 +9,16 @@ # Dmitry Vinokurov , 2015 # Igor Bezukladnikov , 2016 # ilyar, 2016 +# ivan tkachenko , 2016 # Марат , 2015 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-25 18:16+0000\n" -"Last-Translator: ilyar\n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ru/)\n" "MIME-Version: 1.0\n" @@ -48,32 +50,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Обязательно. 255 символов или меньше. Буквы, числа и символы /./-/_" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." -msgstr "Это имя пользователя уже используется." +msgstr "Это имя уже используется." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "Этот адрес почты уже используется." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Токен не подходит ни под одно корректное приглашение." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Пользователь уже зарегистрирован." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "Этот пользователь уже является участником данного проекта" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Ошибка при создании нового пользователя." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Неверный токен" @@ -196,6 +199,15 @@ msgstr "" "Загрузите корректное изображение. Файл, который вы загрузили - либо не " "изображение, либо не корректное изображение." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Страница не является 'последней' и не может быть приведена к int." @@ -253,24 +265,24 @@ msgstr "Неправильные данные." msgid "No input provided" msgstr "Ввод отсутствует" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Нельзя создать новые объект, только существующие объекты могут быть изменены." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Ожидался список объектов." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Не найдено" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Доступ запрещён" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Ошибка приложения на сервере" @@ -280,7 +292,7 @@ msgstr "Ошибка соединения." #: taiga/base/exceptions.py:77 msgid "Malformed request." -msgstr "Неверное сформированный запрос." +msgstr "Неверно сформированный запрос." #: taiga/base/exceptions.py:82 msgid "Incorrect authentication credentials." @@ -345,12 +357,16 @@ msgstr "Ошибка целостности из-за неправильных msgid "Precondition error" msgstr "Ошибка предусловия" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Ошибка в типах фильтров для параметров." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' должно быть целым значением." @@ -502,71 +518,71 @@ msgstr "" " Комментарий: %(comment)s\n" " " -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Нам была нужна хотя бы одна роль" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Необходим дамп-файл" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" -msgstr "Неправильный формат для свалки" +msgstr "Неправильный формат дампа" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" -msgstr "ошибка импорта данных по проекту" +msgstr "ошибка при импорте данных проекта" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" -msgstr "ошибка импорта списка атрибутов проекта" +msgstr "ошибка при импорте списков свойств проекта" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" -msgstr "ошибка импорта значение по умолчанию для атрибутов проекта" +msgstr "ошибка при импорте значений по умолчанию свойств проекта" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" -msgstr "ошибка импорта специальных атрибутов" +msgstr "ошибка при импорте пользовательских свойств" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" -msgstr "ошибка импорта ролей" +msgstr "ошибка при импорте ролей" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" -msgstr "ошибка импорта членства" +msgstr "ошибка при импорте членства" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" -msgstr "ошибка импорта спринтов" +msgstr "ошибка при импорте спринтов" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" -msgstr "ошибка импорта вики-страниц" +msgstr "ошибка при импорте вики-страниц" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" -msgstr "ошибка импорта вики-ссылок" +msgstr "ошибка при импорте вики-ссылок" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" -msgstr "ошибка импорта запросов" +msgstr "ошибка при импорте запросов" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "ошибка импорта историй от пользователей" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "ошибка импорта задач" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "ошибка импорта тэгов" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "ошибка импорта хронологии проекта" @@ -585,9 +601,7 @@ msgid "It contain invalid custom fields." msgstr "Содержит неверные специальные поля" #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Уже есть такое имя для проекта" @@ -839,12 +853,12 @@ msgstr "Необходима аутентификация" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "имя" @@ -856,11 +870,11 @@ msgstr "url иконки" msgid "web" msgstr "веб" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "описание" @@ -874,7 +888,7 @@ msgid "secret key for ciphering the application tokens" msgstr "секретный ключ для шифрования токенов приложения" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "пользователь" @@ -882,11 +896,11 @@ msgstr "пользователь" msgid "application" msgstr "приложение" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "полное имя" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "адрес email" @@ -894,11 +908,11 @@ msgstr "адрес email" msgid "comment" msgstr "комментарий" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -970,8 +984,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Нагрузочный файл не является правильным json-файлом" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Проект не существует" @@ -979,26 +993,26 @@ msgstr "Проект не существует" msgid "Bad signature" msgstr "Плохая подпись" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Указанный элемент не существует" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Статус не существует" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Статус изменён из-за вклада с BitBucket" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Неверная информация о запросе" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1015,17 +1029,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Запрос создан из BitBucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Неправильная информация в комментарии к запросу" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1042,7 +1056,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1312,97 +1326,110 @@ msgstr "Управлять значениями проекта" msgid "Admin roles" msgstr "Управлять ролями" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Список аргументов неполон" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Неправильный формат изображения" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Неверное название шаблона" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Неверное описание шаблона" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" +#: taiga/projects/api.py:356 +msgid "Invalid user id" msgstr "" -"По крайней мере один пользователь должен быть активным администратором." -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "У вас нет разрешения на просмотр." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Частичные обновления не поддерживаются" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "Идентификатор проекта не подходит к этому объекту" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "владелец" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "проект" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "тип содержимого" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "идентификатор объекта" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "изменённая дата" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "приложенный файл" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "устаревшее" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "порядок" @@ -1422,18 +1449,34 @@ msgstr "Специальный" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Текст" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Многострочный текст" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Дата" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1532,7 +1575,7 @@ msgstr "удалено" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Не назначено" @@ -1593,27 +1636,27 @@ msgstr "Заметка о блокировке" msgid "sprint" msgstr "спринт" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "" "У вас нет прав для того чтобы установить такой спринт для этого запроса" -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "" "У вас нет прав для того чтобы установить такой статус для этого запроса" -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "" "У вас нет прав для того чтобы установить такую важность для этого запроса" -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "" "У вас нет прав для того чтобы установить такой приоритет для этого запроса" -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "У вас нет прав для того чтобы установить такой тип для этого запроса" @@ -1667,27 +1710,27 @@ msgstr "Лайк" msgid "Likes" msgstr "Лайки" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "ссылочное имя" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "предполагаемая дата начала" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "предполагаемая дата завершения" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "закрыто" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "доступность" @@ -1714,224 +1757,232 @@ msgstr "параметр '{param}' является обязательным" msgid "'project' parameter is mandatory" msgstr "параметр 'project' является обязательным" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "электронная почта" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "создано" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "идентификатор" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "дополнительный текст к приглашению" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "порядок пользователей" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "Этот пользователем уже является участником проекта" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "очки по умолчанию" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "статусы ПИ по умолчанию" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "статус задачи по умолчанию" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "приоритет по умолчанию" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "важность по умолчанию" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "статус запроса по умолчанию" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "тип запроса по умолчанию" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "лготип" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "участники" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "общее количество вех" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "очки истории" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "активная панель списка задач" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "активная панель kanban" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "активная wiki-панель" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "панель активных запросов" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "система видеоконференций" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "дополнительные данные системы видеоконференций" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "шаблон для создания" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "права анонимов" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "права пользователя" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "личное" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "цвета тэгов" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "дата и время обновления" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "количество" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "активность за неделю" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "активность за месяц" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "активность за год" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "конфигурация модулей" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "архивировано" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "цвет" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "ограничение на активную работу" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "значение" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "роль владельца по умолчанию" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "параметры по умолчанию" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "статусы ПИ" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "очки" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "статусы задач" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "статусы запросов" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "типы запросов" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "приоритеты" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "степени важности" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "роли" @@ -1947,29 +1998,29 @@ msgstr "Все" msgid "None" msgstr "Никаких" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "дата и время создания" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "записи истории" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "уведомить пользователей" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Просмотренные" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Уведомление существует для данных пользователя и проекта" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Неверное значение для уровня уведомлений" @@ -2712,54 +2763,63 @@ msgid "version" msgstr "версия" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Вы не можете покинуть проект если в нём нет других владельцев" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "Этот почтовый адрес уже используется" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Неверная роль для этого проекта" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Параметры по умолчанию" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Статусу пользовательских историй" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Очки" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Статусы задачи" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Статусы запроса" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Типы запроса" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Приоритеты" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Степени важности" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Роли" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Будущий спринт" @@ -2767,16 +2827,27 @@ msgstr "Будущий спринт" msgid "Project End" msgstr "Окончание проекта" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Неверный токен" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "У вас нет прав, чтобы назначить этот спринт для этой задачи." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "" "У вас нет прав, чтобы назначить эту историю от пользователя этой задаче." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "У вас нет прав, чтобы установить этот статус для этой задачи." @@ -2947,6 +3018,236 @@ msgstr "" "\n" "[Taiga] Добавлены к проекту '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3190,17 +3491,17 @@ msgstr "Владелец продукта" msgid "Stakeholder" msgstr "Заинтересованная сторона" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "У вас нет прав чтобы установить спринт для этой пользовательской истории." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" "У вас нет прав чтобы установить статус для этой пользовательской истории." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Генерируется пользовательская история #{ref} - {subject}" @@ -3259,11 +3560,11 @@ msgstr "Голоса" msgid "Vote" msgstr "Голосовать" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "параметр 'content' является обязательным" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "параметр 'project_id' является обязательным" @@ -3275,7 +3576,7 @@ msgstr "последний отредактировавший" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Свертесть с историей API для получения изменений" @@ -3283,129 +3584,145 @@ msgstr "Свертесть с историей API для получения и msgid "Personal info" msgstr "Личные данные" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Права доступа" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Важные даты" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "Этот email уже используется" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Невалидный email" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Неверное имя пользователя или e-mail" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Письмо успешно отправлено!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Неверный токен" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Поле \"текущий пароль\" является обязательным" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Поле \"новый пароль\" является обязательным" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Неверная длина пароля, требуется как минимум 6 символов" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Неверно указан текущий пароль" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "Неверно, вы уверены что токен правильный и не использовался ранее?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Неверно, вы уверены что токен правильный?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "статус суперпользователя" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "Выбранный пользователь имеет все разрешения, ему не чего назначит." -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "имя пользователя" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Обязательно. 30 символов или меньше. Буквы, числа и символы /./-/_" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Введите корректное имя пользователя." -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "активный" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "Выбранный пользователь активен. Отменить выбор для удаления аккаунта." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "биография" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "фотография" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "когда присоединился" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "язык по умолчанию" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "тема по умолчанию" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "временная зона по умолчанию" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "установить цвета для тэгов" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "email токен" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "новый email адрес" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "разрешения" @@ -3417,10 +3734,26 @@ msgstr "невалидный" msgid "Invalid username. Try with a different one." msgstr "Неверное имя пользователя. Попробуйте другое." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Имя пользователя или пароль не соответствуют пользователю." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po index a97be689..49e7041c 100644 --- a/taiga/locale/sv/LC_MESSAGES/django.po +++ b/taiga/locale/sv/LC_MESSAGES/django.po @@ -8,9 +8,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-22 12:10+0000\n" -"Last-Translator: Taiga Dev Team \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/sv/)\n" "MIME-Version: 1.0\n" @@ -40,32 +41,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Kräver färre än 255 tecken. Kan vara tecken, nummer och /./-/_." -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Användarnamnet används redan" -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "E-postadressen används redan" -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Förekomsten passar inte invitationen. " -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Användaren finns redan." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Ett fel uppstod når användaren skapades. " #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Felaktig förekomst. " @@ -186,6 +188,15 @@ msgstr "" "Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild " "eller en skadad bild." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" @@ -244,24 +255,24 @@ msgstr "Felaktigt data" msgid "No input provided" msgstr "Inga indata" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Det går inte att skapa ett nytt objekt, endast befintliga poster uppdateras." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Förväntad lista på poster." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Hittade inte" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "Du har inte behöriget" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Serverprogramfel." @@ -336,12 +347,16 @@ msgstr "Integritetsfel för felaktiga eller ogiltiga argument" msgid "Precondition error" msgstr "Förutsättningsfel" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Fel i filterparametertyper." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'Projektet\" måste vara ett heltal." @@ -477,71 +492,71 @@ msgid "" " " msgstr "" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "Vi behöver minst en roll" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "Behöver en hämtningsfil" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Invalid hämtningsfilformat" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "fel vid import av projektdata" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "fel vid import av en lista på projektegenskaper" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "fel vid import av standard projektegenskapsvärden" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "fel vid import av anpassade egenskaper" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "fel vid importering av roller" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "fel vid import av medlemskap" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "felaktig import av sprintar" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "vel vid import av wiki-sidor" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "fel vid import av wiki-länkar" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "fel vid import av ärenden" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "fel vid import av användarhistorier" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "fel vid import av uppgifter" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "fel vid importering av taggar" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "fel vid importering av tidslinje" @@ -560,9 +575,7 @@ msgid "It contain invalid custom fields." msgstr "Innehåller felaktigt anpassad fält." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Namnet är upprepad för projektet" @@ -728,12 +741,12 @@ msgstr "Verifiering krävs" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "namn" @@ -745,11 +758,11 @@ msgstr "Ikonlänk" msgid "web" msgstr "Internet" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "beskrivning" @@ -763,7 +776,7 @@ msgid "secret key for ciphering the application tokens" msgstr "hemlig nyckel för kryptering av programtecken " #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "användare" @@ -771,11 +784,11 @@ msgstr "användare" msgid "application" msgstr "program" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "hela namnet" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "e-postadress" @@ -783,11 +796,11 @@ msgstr "e-postadress" msgid "comment" msgstr "kommentera" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -843,8 +856,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Datasträngen är inte korrekt json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Projektet existerar inte" @@ -852,26 +865,26 @@ msgstr "Projektet existerar inte" msgid "Bad signature" msgstr "Dålig signatur" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Referenselementet existerar inte" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Statusen existerar inte" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Status ändrad från BitBucket skrivs in" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Felaktig ärendeinformation" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -882,17 +895,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Ärende skapades från BitBucket." -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Felaktigt kommentarinformation för ärendet" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -903,7 +916,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1153,96 +1166,110 @@ msgstr "Administrera projektvärden" msgid "Admin roles" msgstr "Administratorroller" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Felaktiga argument" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Felaktigt bildformat" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Inget giltigt mallnamn" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Inte giltigt mallbeskrivning" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Åtminstone en av användarna måste vara en aktiv administrator" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Du har inte behörighet att se det. " -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Delvisa uppdateringar stöds inte. " -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "Projekt-ID stämmer inte mellan objekt och projekt" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "ägare" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "projekt" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "innehållstyp" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "objekt-ID" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "ändrad datum" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "bifogad fil" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "undviks" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "sortera" @@ -1262,18 +1289,34 @@ msgstr "Anpassa" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Text" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Text med flera rader" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Datum" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1372,7 +1415,7 @@ msgstr "borttaget" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Ej tilldelad" @@ -1433,23 +1476,23 @@ msgstr "blockerad notering" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Du har inte behörighet att sätta sprinten till det här ärendet." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Du har inte behörighet att sätta status till det här ärendet. " -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Du har inte behörighet att sätta allvarsgrad till det här ärendet. " -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Du har inte behörighet att sätta prioriteten för det här ärendet. " -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Du har inte behörighet att lägga till typen till ärendet. " @@ -1503,27 +1546,27 @@ msgstr "Gillar" msgid "Likes" msgstr "Gillar" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "slugg" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "Beräknad startdatum" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "Beräknad slutdato" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "är stängd" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponerar" @@ -1548,224 +1591,232 @@ msgstr "'{param}' parameter är obligatoriskt" msgid "'project' parameter is mandatory" msgstr "'project' parameter är obligatoriskt" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "e-post" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "skapa som" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "textsträng" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "Invitation - extra text" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "användarorder" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "Användaren är redan medlem i projekt" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "standardpoäng" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "standard US-poäng" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "standard status för uppgift" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "standard prioritet" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "standard allvarsgrad" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "standard status för ärende" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "standard typ för ärende" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "medlemmar" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "totalt antal milstolpar" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "totalt antal historiepoäng" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "aktivt panel för inkorg" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "aktiv kanban-panel" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "aktiv wiki-panel" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "aktiv panel för ärenden" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "videokonferensssystem" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "videokonferens - extra data" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "mall skapas" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "anonyma rättigheter" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "användarbehörigheter" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "är privat" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "färger för taggar" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "uppdaterad dato och tid" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "räkna" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "konfigurera moduler" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "är arkiverad" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "färg" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "begränsad arbete pågår" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "värde" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "ägarens standardroll" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "standard val" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "US statuser" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "poäng" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "statuser för uppgifter" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "status för ärenden" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "ärendentyper" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "prioriteter" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "allvarsgrad" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "roller" @@ -1781,29 +1832,29 @@ msgstr "Alla" msgid "None" msgstr "Ingen" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "skapad dato och tid" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "historienotat" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "notifiera användare" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "Visad" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Notifiering finns för användaren och projektet" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Felaktigt värde för notifieringen" @@ -2299,54 +2350,63 @@ msgid "version" msgstr "version" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Du kan inte lämna projketet om det inte är flera projektägare" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "E-postadressen är redan använd" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Fel roll for projektet" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Standardval" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Status för användarhistorien" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Poäng" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Status för uppgifter" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Status för ärenden" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Ärendetyper" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Prioritet" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Allvarsgrad" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Roller" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Framtidig sprint" @@ -2354,15 +2414,26 @@ msgstr "Framtidig sprint" msgid "Project End" msgstr "Projektslut" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Textsträngen är ogiltig" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "Du har inte behörighet åt att sätta sprinten till en uppgift" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "Du har inte behörighet att sätta använderhistorien till en uppgift." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "Du har inte behörighet att sätta status till en uppgift. " @@ -2490,6 +2561,236 @@ msgid "" "[Taiga] Added to the project '%(project)s'\n" msgstr "" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -2732,18 +3033,18 @@ msgstr "Produktägare" msgid "Stakeholder" msgstr "Intressent" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Du har inte behörighet för att lägga sprinten till den här användarhistorien" -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "" "Du har inte behörighet till att sätta den här statusen till " "användarhistorien." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Skapar användarhistorie #{ref} - {subject}" @@ -2802,11 +3103,11 @@ msgstr "Röster" msgid "Vote" msgstr "Rösta" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'content' parametern är obligatoriskt" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametern är obligatoriskt" @@ -2818,7 +3119,7 @@ msgstr "senastste ändring" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "Kolla historie API för exakt skillnad" @@ -2826,66 +3127,66 @@ msgstr "Kolla historie API för exakt skillnad" msgid "Personal info" msgstr "Personalinformation" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "Behörigheter" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Viktiga datum" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "E-post-dublett" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Ingen giltig e-postadress" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Ogiltigt användarnamn eller e-postadress" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "E-posten skickades korrekt" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Textsträngen är ogiltig" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "Parameter för nuvarande lösenord krävs" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Parameter för nytt lösenord krävs" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Felaktig längd på lösenord. Minst 6 alfanumeriska tecken krävs." -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "Fel lösenord" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Fel. Är du säker på att strängen är korrekt och att du inte har använt det " "tidigare?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Fel, är du säker på att textsträngen är korrekt? " -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "status för administratorn" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -2893,25 +3194,25 @@ msgstr "" "Anger om användaren har alla behörigheter utan att uttryckligen tilldela " "dem. " -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "användarnamn" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Obligatoriskt. 30 eller färre alfanumeriska tecken, bokstäver och /./-/_ . " -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Skriv in ett giltigt användarnamn" -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "aktiv" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2919,43 +3220,59 @@ msgstr "" "Anger om användaren ska betraktas som aktiv. Avmarkera detta i stället för " "att ta bort kontot." -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biografi" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "foto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "blev medlem datum" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "standardspråk" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "standardtema" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "standard tidzon" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "farglägg taggar" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "e-poststräng" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "ny e-postadress" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "behörigheter" @@ -2967,10 +3284,26 @@ msgstr "felaktigt" msgid "Invalid username. Try with a different one." msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Användarnamn eller lösenord passar inte." +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po index b0bfc3c0..d5aa3d2a 100644 --- a/taiga/locale/tr/LC_MESSAGES/django.po +++ b/taiga/locale/tr/LC_MESSAGES/django.po @@ -10,9 +10,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-26 14:17+0000\n" -"Last-Translator: Mert Torun \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/tr/)\n" "MIME-Version: 1.0\n" @@ -43,32 +44,33 @@ msgid "" msgstr "" "Zorunlu. 255 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "Kullanıcı adı zaten kullanımda." -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "E-posta zaten kullanımda." -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "Kupon geçerli hiç bir davetle uyuşmuyor." -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "Kullanıcı zaten kayıtlı." -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "Bu kullanızı halihazırda zaten projenin bir üyesi." -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi." #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "Geçersiz kupon" @@ -194,6 +196,15 @@ msgstr "" "Geçerli bir resim yükleyin. Yüklenen dosya ya bozulmuş bir resim ya da bir " "resim dosyası değil." +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Sayfa 'last'(son) değil, tamsayıya da çevrilemiyor." @@ -251,23 +262,23 @@ msgstr "Geçersiz veri" msgid "No input provided" msgstr "Girdi sağlanmadı" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "Yeni bir madde oluşturlamıyor, sadece var olanlar güncellenebilir." -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "Bir madde listesi bekleniyor." -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "Bulunamadı" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "İzin verilmedi" -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "Sunucu uygulaması hatası" @@ -342,12 +353,16 @@ msgstr "Hatalı ya da geçersiz parametreler için Bütünlük Hatası " msgid "Precondition error" msgstr "Ön şart hatası" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Parametre tipleri filtresinde hata." -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "'project' değeri numerik olmalı." @@ -487,71 +502,71 @@ msgstr "" "\n" "Yorumlar: %(comment)s" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "En azından bir role ihtiyacımız var" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "İhtiyaç duyulan döküm dosyası" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "Geçersiz döküm biçemi" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "İçeri aktarılan proje verisinde hata" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "özel öznitelikler içeri aktarılırken hata" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "İçeri aktarılan rollerde hata" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "İçeri aktarılan üyeliklerde hata" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "İçeri aktarılan sprintlerde hata" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "İçeri aktarılan wiki sayfalarında hata" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "İçeri aktarılan wiki bağlantılarında hata" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "İçeri aktarılan taleplerde hata" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "İçeri aktarılan kullanıcı hikayelerinde hata" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "İçeri aktarılan görevlerde hata" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "İçeri aktarılan etiketlerde hata" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "zaman çizelgesi içeri aktarılırken hata" @@ -570,9 +585,7 @@ msgid "It contain invalid custom fields." msgstr "Geçersiz özel alanlar içeriyor." #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "Aynı isimde proje bulunmakta" @@ -823,12 +836,12 @@ msgstr "Kimlik doğrulama gerekli" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "isim" @@ -840,11 +853,11 @@ msgstr "İkon url" msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "tanı" @@ -858,7 +871,7 @@ msgid "secret key for ciphering the application tokens" msgstr "" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "kullanıcı" @@ -866,11 +879,11 @@ msgstr "kullanıcı" msgid "application" msgstr "uygulama" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "tam ad" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "e-posta adresi" @@ -878,11 +891,11 @@ msgstr "e-posta adresi" msgid "comment" msgstr "yorum" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -952,8 +965,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "Proje mevcut değil." @@ -961,26 +974,26 @@ msgstr "Proje mevcut değil." msgid "Bad signature" msgstr "Kötü imza" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "Referans gösterilmiş varlık mevcut değil" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "Durum mevcut değil" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "Bitbucket commiti ile durum değişti" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "Geçersiz talep bilgisi" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -991,17 +1004,17 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "Bitbucket ten oluşturulan talep" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "Geçersiz talep yorum bilgisi" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1012,7 +1025,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1260,96 +1273,110 @@ msgstr "Admin proje değerleri" msgid "Admin roles" msgstr "Yönetici rolleri" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Eksik parametreq" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "Geçersiz resim biçemi" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "Geçersiz şablon adı" -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "Geçersiz şablon tanımı" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "Kullanıcılardan en az biri aktif yönetici olmalıdır" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "Görebilmek için yetkiniz yok." -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "Kısmi güncellemeler desteklenmiyor" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "sahip" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "proje" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "içerik tipi" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "nesne id" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "düzenleme tarihi" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "eklenmiş dosya" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "kaldırıldı" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "sıra" @@ -1369,18 +1396,34 @@ msgstr "Özel" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "Metin" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "Çoklu-satır metin" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "Tarih" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1479,7 +1522,7 @@ msgstr "silindi" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "Atanmamış" @@ -1540,23 +1583,23 @@ msgstr "engellenmiş not" msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "Bu talep için bu sprinti ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "Bu talep için bu durumu ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "Bu talep için bu kritiklik derecesini ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "Bu talep için bu öncelik durumunu ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "Bu talep için bu tipi ayarlamaya yetkiniz yok." @@ -1610,27 +1653,27 @@ msgstr "Beğen" msgid "Likes" msgstr "Beğeniler" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "satır" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "yaklaşık başlama tarihi" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "yaklaşık bitiş tarihi" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "kapatılmış" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "taşınabilirlik" @@ -1655,224 +1698,232 @@ msgstr "'{param}' parametresi zorunlu" msgid "'project' parameter is mandatory" msgstr "'proje' parametresi zorunlu" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "e-posta" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "kupon" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "Davetiye ekstra metni" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "kullanıcı sırası" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "Kullanıcı zaten projenin üyesi" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "varsayılan puanlar" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "varsayılan KH durumu" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "varsayılan görev durumu" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "varsayılan öncelik" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "varsayılan önem derecesi" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "varsayılan talep durumu" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "varsayılan talep tipi" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "üyeler" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "aşamaların toplamı" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "toplam hikaye puanı" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "aktif birikmiş iler paneli" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "aktif kanban paneli" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "aktif wiki paneli" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "aktif talep paneli" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "video konferans sistemi" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "videokonferans ekstra verisi" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "oluşturma şablonu" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "anonim izinler" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "kullanıcı izinleri" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "gizli" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr "vitrinde" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "insan arıyor" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "etiket renkleri" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "yükleme tarih-saati" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "sayı" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "geçen hafta fanları" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "geçen ayın fanları" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "geçen yılın fanları" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "geçen haftanın aktiviteleri" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "geçen ayın aktiviteleri" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "geçen yılın aktiviteleri" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "modül ayarları" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "arşivlenmiş" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "renk" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "değer" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "varsayılan sahip rolü" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "varsayılan ayarlar" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "kh durumları" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "puanlar" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "görev durumları" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "talep durumları" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "talep tipleri" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "öncelikler" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "önem durumları" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "roller" @@ -1888,29 +1939,29 @@ msgstr "Hepsi" msgid "None" msgstr "Hiçbiri" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "oluşturma tarih-saati" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "tarihçe girdileri" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "kullanıcıları bilgilendir" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "İzlenen" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "Belirtilen kullanıcı ve proje için bilgilendirme mevcut" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "Bildirim düzeyi için geçersiz değer" @@ -2472,54 +2523,63 @@ msgid "version" msgstr "sürüm" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "Eğer sizden başka sahip/yönetici kalmadıysa projeyi terk edemezsiniz" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "E-posta adresi önceden alınmış" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "Proje için geçersiz rol" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "Varsayılan ayarlar" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "Kullanıcı hikayelerinin durumları" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "Puanlar" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "Görevlerin durumları" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "Taleplerin durumları" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "Taleplerin tipleri" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "Öncelikler" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "Önem dereceleri" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "Roller" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "Gelecek sprint" @@ -2527,15 +2587,26 @@ msgstr "Gelecek sprint" msgid "Project End" msgstr "Proje Sonu" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "Kupon geçersiz" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "Bu görev için sprint ayarlamanız için izniniz yok." -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "Bu görev için kullanıcı hikayesi ayarlama izniniz yok." -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "Bu görev için bu durumu ayarlama izniniz yok." @@ -2675,6 +2746,236 @@ msgstr "" "\n" "[Taiga] '%(project)s' Projesine eklendi\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -2907,15 +3208,15 @@ msgstr "Ürün Sahibi" msgid "Stakeholder" msgstr "Paydaş" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "Bu kullanıcı hikayesine bu sprinti ayarlama izniniz yok." -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "Bu kullanıcı hikayesine bu durumu ayarlama yetkiniz yok." -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" @@ -2974,11 +3275,11 @@ msgstr "Oylar" msgid "Vote" msgstr "Oy" -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'content' parametresi zorunlu" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametresi zorunlu" @@ -2990,7 +3291,7 @@ msgstr "son düzenleyen" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "" @@ -2998,132 +3299,148 @@ msgstr "" msgid "Personal info" msgstr "Kişisel bilgi" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "İzinler" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "Önemli tarihler" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "Geçersiz e-posta" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "Geçersiz kullanıcı adı ya da e-posta" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "Posta başarıyla gönderildi!" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "Kupon geçersiz" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "Yeni parola parametresi gerekli" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "Geçersiz parola uzunluğu, en az 6 karaktere ihtiyaç var" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Geçersiz geçerli bir kupona sahip olduğunuzdan ve bu kuponu daha önce " "kullanmadığınızdan emin misiniz?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "Geçersiz, kuponun doğru olduğuna emin misin?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "superuser durumu" -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "kullanıcı adı" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Zorunlu. 30 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "Geçerli bir kullanıcı adı girin." -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "aktif" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "biyografi" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "fotoğraf" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "katılma tarihi" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "varsayılan dil" -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "varsayılan tema" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "varsayılan saat dilimi" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "etiketleri renklendir" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "e-posta kuponu" -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "yeni e-posta adresi" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "izinler" @@ -3135,10 +3452,26 @@ msgstr "Geçersiz" msgid "Invalid username. Try with a different one." msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin." -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "Kullanıcı adı veya parola kullanıcıyla uyuşmuyor" +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po index dacffc4b..2136848c 100644 --- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po +++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po @@ -11,9 +11,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-29 10:21+0100\n" -"PO-Revision-Date: 2016-01-26 03:45+0000\n" -"Last-Translator: Chi-Hsun Tsai \n" +"POT-Creation-Date: 2016-04-01 11:09+0200\n" +"PO-Revision-Date: 2016-03-30 10:59+0000\n" +"Last-Translator: Alejandro Alonso Fernández \n" "Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/zh-Hant/)\n" "MIME-Version: 1.0\n" @@ -43,32 +44,33 @@ msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "必填。最多255字元(可為數字,字母,符號....)" -#: taiga/auth/services.py:74 +#: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "本用戶名稱已被註冊" -#: taiga/auth/services.py:77 +#: taiga/auth/services.py:78 msgid "Email is already in use." msgstr "本電子郵件已使用" -#: taiga/auth/services.py:93 +#: taiga/auth/services.py:94 msgid "Token not matches any valid invitation." msgstr "代碼與任何有效的邀請不相符" -#: taiga/auth/services.py:121 +#: taiga/auth/services.py:122 msgid "User is already registered." msgstr "使用者已被註冊。" -#: taiga/auth/services.py:145 +#: taiga/auth/services.py:146 msgid "This user is already a member of the project." msgstr "使用者已是專案成員" -#: taiga/auth/services.py:171 +#: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "無法創建新使用者" #: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 +#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 +#: taiga/projects/api.py:397 msgid "Invalid token" msgstr "無效的代碼 " @@ -180,6 +182,15 @@ msgid "" "corrupted image." msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞" +#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 +#: taiga/hooks/api.py:68 taiga/projects/api.py:629 +#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 +#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 +#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 +#: taiga/webhooks/api.py:67 +msgid "Blocked element" +msgstr "" + #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." msgstr "頁數不是最後,或者它無法轉成整數 " @@ -237,23 +248,23 @@ msgstr "無效的資料" msgid "No input provided" msgstr "無輸入提供" -#: taiga/base/api/serializers.py:572 +#: taiga/base/api/serializers.py:575 msgid "Cannot create a new item, only existing items may be updated." msgstr "無法建立新項目,只能更新現有項目" -#: taiga/base/api/serializers.py:583 +#: taiga/base/api/serializers.py:586 msgid "Expected a list of items." msgstr "期待的項目清單" -#: taiga/base/api/views.py:124 +#: taiga/base/api/views.py:125 msgid "Not found" msgstr "找不到" -#: taiga/base/api/views.py:127 +#: taiga/base/api/views.py:128 msgid "Permission denied" msgstr "許可遭拒絕 " -#: taiga/base/api/views.py:475 +#: taiga/base/api/views.py:476 msgid "Server application error" msgstr "伺服器應用出錯" @@ -328,12 +339,16 @@ msgstr "因錯誤或無效參數,一致性出錯" msgid "Precondition error" msgstr "前提出錯" -#: taiga/base/filters.py:78 +#: taiga/base/exceptions.py:217 +msgid "No room left for more projects." +msgstr "" + +#: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "過濾參數類型出錯" -#: taiga/base/filters.py:132 taiga/base/filters.py:231 -#: taiga/projects/filters.py:43 +#: taiga/base/filters.py:133 taiga/base/filters.py:232 +#: taiga/projects/filters.py:59 msgid "'project' must be an integer value." msgstr "專案須為整數值" @@ -484,71 +499,71 @@ msgstr "" "\n" "評論: %(comment)s" -#: taiga/export_import/api.py:105 +#: taiga/export_import/api.py:114 msgid "We needed at least one role" msgstr "我們至少需要一個角色" -#: taiga/export_import/api.py:199 +#: taiga/export_import/api.py:216 msgid "Needed dump file" msgstr "需要的堆存檔案" -#: taiga/export_import/api.py:206 +#: taiga/export_import/api.py:224 msgid "Invalid dump format" msgstr "無效堆存格式" -#: taiga/export_import/dump_service.py:97 +#: taiga/export_import/dump_service.py:106 msgid "error importing project data" msgstr "滙入重要專案資料出錯" -#: taiga/export_import/dump_service.py:110 +#: taiga/export_import/dump_service.py:119 msgid "error importing lists of project attributes" msgstr "滙入標籤出錯" -#: taiga/export_import/dump_service.py:115 +#: taiga/export_import/dump_service.py:124 msgid "error importing default project attributes values" msgstr "滙入預設專案屬性數值出錯" -#: taiga/export_import/dump_service.py:125 +#: taiga/export_import/dump_service.py:134 msgid "error importing custom attributes" msgstr "滙入客制性屬出錯" -#: taiga/export_import/dump_service.py:130 +#: taiga/export_import/dump_service.py:139 msgid "error importing roles" msgstr "滙入角色出錯" -#: taiga/export_import/dump_service.py:145 +#: taiga/export_import/dump_service.py:154 msgid "error importing memberships" msgstr "滙入成員資格出錯" -#: taiga/export_import/dump_service.py:150 +#: taiga/export_import/dump_service.py:159 msgid "error importing sprints" msgstr "滙入衝刺任務出錯" -#: taiga/export_import/dump_service.py:155 +#: taiga/export_import/dump_service.py:164 msgid "error importing wiki pages" msgstr "滙入維基頁出錯" -#: taiga/export_import/dump_service.py:160 +#: taiga/export_import/dump_service.py:169 msgid "error importing wiki links" msgstr "滙入維基連結出錯" -#: taiga/export_import/dump_service.py:165 +#: taiga/export_import/dump_service.py:174 msgid "error importing issues" msgstr "滙入問題出錯" -#: taiga/export_import/dump_service.py:170 +#: taiga/export_import/dump_service.py:179 msgid "error importing user stories" msgstr "滙入使用者故事出錯" -#: taiga/export_import/dump_service.py:175 +#: taiga/export_import/dump_service.py:184 msgid "error importing tasks" msgstr "滙入任務出錯" -#: taiga/export_import/dump_service.py:180 +#: taiga/export_import/dump_service.py:189 msgid "error importing tags" msgstr "滙入標籤出錯" -#: taiga/export_import/dump_service.py:184 +#: taiga/export_import/dump_service.py:193 msgid "error importing timelines" msgstr "滙入時間軸出錯" @@ -567,9 +582,7 @@ msgid "It contain invalid custom fields." msgstr "包括無效慣例欄位" #: taiga/export_import/serializers.py:528 -#: taiga/projects/milestones/serializers.py:57 taiga/projects/serializers.py:70 -#: taiga/projects/serializers.py:95 taiga/projects/serializers.py:125 -#: taiga/projects/serializers.py:168 +#: taiga/projects/mixins/serializers.py:38 msgid "Name duplicated for the project" msgstr "專案的名稱被複製了" @@ -820,12 +833,12 @@ msgstr "要求取得授權" #: taiga/external_apps/models.py:34 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:163 -#: taiga/projects/models.py:477 taiga/projects/models.py:516 -#: taiga/projects/models.py:541 taiga/projects/models.py:578 -#: taiga/projects/models.py:601 taiga/projects/models.py:624 -#: taiga/projects/models.py:659 taiga/projects/models.py:682 -#: taiga/users/models.py:251 taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 +#: taiga/projects/models.py:472 taiga/projects/models.py:511 +#: taiga/projects/models.py:536 taiga/projects/models.py:573 +#: taiga/projects/models.py:596 taiga/projects/models.py:619 +#: taiga/projects/models.py:654 taiga/projects/models.py:677 +#: taiga/users/models.py:289 taiga/webhooks/models.py:28 msgid "name" msgstr "姓名" @@ -837,11 +850,11 @@ msgstr "網址圖標" msgid "web" msgstr "網頁" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:75 +#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 #: taiga/projects/custom_attributes/models.py:36 #: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:167 -#: taiga/projects/models.py:686 taiga/projects/tasks/models.py:61 +#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 +#: taiga/projects/models.py:681 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:92 msgid "description" msgstr "描述" @@ -855,7 +868,7 @@ msgid "secret key for ciphering the application tokens" msgstr "應用程式的密碼字符數列" #: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:85 taiga/projects/votes/models.py:51 +#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 msgid "user" msgstr "使用者" @@ -863,11 +876,11 @@ msgstr "使用者" msgid "application" msgstr "應用程式" -#: taiga/feedback/models.py:24 taiga/users/models.py:117 +#: taiga/feedback/models.py:24 taiga/users/models.py:138 msgid "full name" msgstr "全名" -#: taiga/feedback/models.py:26 taiga/users/models.py:112 +#: taiga/feedback/models.py:26 taiga/users/models.py:133 msgid "email address" msgstr "電子郵件" @@ -875,11 +888,11 @@ msgstr "電子郵件" msgid "comment" msgstr "評論" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:62 +#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:48 taiga/projects/models.py:174 -#: taiga/projects/models.py:688 taiga/projects/notifications/models.py:87 +#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 +#: taiga/projects/models.py:683 taiga/projects/notifications/models.py:88 #: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 #: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 #: taiga/userstorage/models.py:28 @@ -948,8 +961,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "載荷為無效json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:142 -#: taiga/projects/tasks/api.py:85 taiga/projects/userstories/api.py:110 +#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 +#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 msgid "The project doesn't exist" msgstr "專案不存在" @@ -957,26 +970,26 @@ msgstr "專案不存在" msgid "Bad signature" msgstr "錯誤簽名" -#: taiga/hooks/bitbucket/event_hooks.py:85 taiga/hooks/github/event_hooks.py:76 +#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 #: taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "參考元素不存在" -#: taiga/hooks/bitbucket/event_hooks.py:92 taiga/hooks/github/event_hooks.py:83 +#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 #: taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "狀態不存在" -#: taiga/hooks/bitbucket/event_hooks.py:98 +#: taiga/hooks/bitbucket/event_hooks.py:95 msgid "Status changed from BitBucket commit" msgstr "來自BitBucket 投入的狀態更新" -#: taiga/hooks/bitbucket/event_hooks.py:127 +#: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "無效的問題資訊" -#: taiga/hooks/bitbucket/event_hooks.py:143 +#: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format msgid "" "Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -993,17 +1006,17 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/bitbucket/event_hooks.py:154 +#: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." msgstr "來自BitBucket的問題:" -#: taiga/hooks/bitbucket/event_hooks.py:178 +#: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" msgstr "無效的議題評論資訊" -#: taiga/hooks/bitbucket/event_hooks.py:186 +#: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format msgid "" "Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " @@ -1020,7 +1033,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/bitbucket/event_hooks.py:197 +#: taiga/hooks/bitbucket/event_hooks.py:194 #, python-brace-format msgid "" "Comment From BitBucket:\n" @@ -1286,96 +1299,110 @@ msgstr "管理員專案數值" msgid "Admin roles" msgstr "管理員角色" -#: taiga/projects/api.py:154 taiga/users/api.py:219 +#: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "不完整參數" -#: taiga/projects/api.py:158 taiga/users/api.py:224 +#: taiga/projects/api.py:169 taiga/users/api.py:225 msgid "Invalid image format" msgstr "無效的圖片檔案" -#: taiga/projects/api.py:286 +#: taiga/projects/api.py:230 msgid "Not valid template name" msgstr "非有效樣板名稱 " -#: taiga/projects/api.py:289 +#: taiga/projects/api.py:233 msgid "Not valid template description" msgstr "無效樣板描述" -#: taiga/projects/api.py:547 taiga/projects/serializers.py:261 -msgid "At least one of the user must be an active admin" -msgstr "至少需有一位使用者擔任管理員" +#: taiga/projects/api.py:356 +msgid "Invalid user id" +msgstr "" -#: taiga/projects/api.py:577 +#: taiga/projects/api.py:362 +msgid "The user doesn't exist" +msgstr "" + +#: taiga/projects/api.py:366 +msgid "The user must be already a project member" +msgstr "" + +#: taiga/projects/api.py:668 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" + +#: taiga/projects/api.py:708 msgid "You don't have permisions to see that." msgstr "您無觀看權限" -#: taiga/projects/attachments/api.py:48 +#: taiga/projects/attachments/api.py:51 msgid "Partial updates are not supported" msgstr "不支援部份更新" -#: taiga/projects/attachments/api.py:63 +#: taiga/projects/attachments/api.py:66 msgid "Project ID not matches between object and project" msgstr "專案ID不符合物件與專案" -#: taiga/projects/attachments/models.py:53 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:179 -#: taiga/projects/notifications/models.py:60 taiga/projects/tasks/models.py:38 +#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 +#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 #: taiga/userstorage/models.py:26 msgid "owner" msgstr "所有者" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:44 -#: taiga/projects/models.py:465 taiga/projects/models.py:491 -#: taiga/projects/models.py:522 taiga/projects/models.py:551 -#: taiga/projects/models.py:584 taiga/projects/models.py:607 -#: taiga/projects/models.py:634 taiga/projects/models.py:665 -#: taiga/projects/notifications/models.py:72 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:42 +#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:460 taiga/projects/models.py:486 +#: taiga/projects/models.py:517 taiga/projects/models.py:546 +#: taiga/projects/models.py:579 taiga/projects/models.py:602 +#: taiga/projects/models.py:629 taiga/projects/models.py:660 +#: taiga/projects/notifications/models.py:73 +#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:264 +#: taiga/projects/wiki/models.py:68 taiga/users/models.py:302 msgid "project" msgstr "專案" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:42 msgid "content type" msgstr "內容類型" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:44 msgid "object id" msgstr "物件ID" -#: taiga/projects/attachments/models.py:65 +#: taiga/projects/attachments/models.py:50 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:51 -#: taiga/projects/models.py:177 taiga/projects/models.py:691 +#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 +#: taiga/projects/models.py:160 taiga/projects/models.py:686 #: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 msgid "modified date" msgstr "修改日期" -#: taiga/projects/attachments/models.py:70 +#: taiga/projects/attachments/models.py:55 msgid "attached file" msgstr "附加檔案" -#: taiga/projects/attachments/models.py:72 +#: taiga/projects/attachments/models.py:57 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:59 msgid "is deprecated" msgstr "棄用" -#: taiga/projects/attachments/models.py:76 +#: taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:57 taiga/projects/models.py:481 -#: taiga/projects/models.py:518 taiga/projects/models.py:545 -#: taiga/projects/models.py:580 taiga/projects/models.py:603 -#: taiga/projects/models.py:628 taiga/projects/models.py:661 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:259 +#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476 +#: taiga/projects/models.py:513 taiga/projects/models.py:540 +#: taiga/projects/models.py:575 taiga/projects/models.py:598 +#: taiga/projects/models.py:623 taiga/projects/models.py:656 +#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297 msgid "order" msgstr "次序" @@ -1395,18 +1422,34 @@ msgstr "自定" msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/choices.py:26 +#: taiga/projects/choices.py:32 +msgid "This project is blocked due to payment failure" +msgstr "" + +#: taiga/projects/choices.py:33 +msgid "This project is blocked by admin staff" +msgstr "" + +#: taiga/projects/choices.py:34 +msgid "This project is blocked because the owner left" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 msgid "Text" msgstr "單行文字" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/custom_attributes/choices.py:28 msgid "Multi-Line Text" msgstr "多行列文字" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Date" msgstr "日期" +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Url" +msgstr "" + #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 msgid "type" @@ -1505,7 +1548,7 @@ msgstr "移除 " #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:56 taiga/projects/services/stats.py:57 +#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 msgid "Unassigned" msgstr "無指定" @@ -1566,23 +1609,23 @@ msgstr "封鎖筆記" msgid "sprint" msgstr "衝刺任務" -#: taiga/projects/issues/api.py:163 +#: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." msgstr "您無權限設定此問題的衝刺任務" -#: taiga/projects/issues/api.py:167 +#: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." msgstr "您無權限設定此問題的狀態" -#: taiga/projects/issues/api.py:171 +#: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." msgstr "您無權限設定此問題的嚴重性" -#: taiga/projects/issues/api.py:175 +#: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." msgstr "您無權限設定此問題的優先性" -#: taiga/projects/issues/api.py:179 +#: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." msgstr "您無權限設定此問題的類型" @@ -1636,27 +1679,27 @@ msgstr "喜歡" msgid "Likes" msgstr "喜歡" -#: taiga/projects/milestones/models.py:40 taiga/projects/models.py:165 -#: taiga/projects/models.py:479 taiga/projects/models.py:543 -#: taiga/projects/models.py:626 taiga/projects/models.py:684 -#: taiga/projects/wiki/models.py:32 taiga/users/models.py:253 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 +#: taiga/projects/models.py:474 taiga/projects/models.py:538 +#: taiga/projects/models.py:621 taiga/projects/models.py:679 +#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291 msgid "slug" msgstr "代稱" -#: taiga/projects/milestones/models.py:45 +#: taiga/projects/milestones/models.py:46 msgid "estimated start date" msgstr "预計開始日期" -#: taiga/projects/milestones/models.py:46 +#: taiga/projects/milestones/models.py:47 msgid "estimated finish date" msgstr "預計完成日期" -#: taiga/projects/milestones/models.py:53 taiga/projects/models.py:483 -#: taiga/projects/models.py:547 taiga/projects/models.py:630 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478 +#: taiga/projects/models.py:542 taiga/projects/models.py:625 msgid "is closed" msgstr "被關閉" -#: taiga/projects/milestones/models.py:55 +#: taiga/projects/milestones/models.py:56 msgid "disponibility" msgstr "disponibility" @@ -1681,224 +1724,232 @@ msgstr "'{param}' 參數為必要" msgid "'project' parameter is mandatory" msgstr "'project'參數為必要" -#: taiga/projects/models.py:95 +#: taiga/projects/models.py:78 msgid "email" msgstr "電子郵件" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:80 msgid "create at" msgstr "創建於" -#: taiga/projects/models.py:99 taiga/users/models.py:134 +#: taiga/projects/models.py:82 taiga/users/models.py:155 msgid "token" msgstr "代號" -#: taiga/projects/models.py:105 +#: taiga/projects/models.py:88 msgid "invitation extra text" msgstr "額外文案邀請" -#: taiga/projects/models.py:108 +#: taiga/projects/models.py:91 msgid "user order" msgstr "使用者次序" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:101 msgid "The user is already member of the project" msgstr "使用者已是專案成員" -#: taiga/projects/models.py:133 +#: taiga/projects/models.py:116 msgid "default points" msgstr "預設點數" -#: taiga/projects/models.py:137 +#: taiga/projects/models.py:120 msgid "default US status" msgstr "預設使用者故事狀態" -#: taiga/projects/models.py:141 +#: taiga/projects/models.py:124 msgid "default task status" msgstr "預設任務狀態" -#: taiga/projects/models.py:144 +#: taiga/projects/models.py:127 msgid "default priority" msgstr "預設優先性" -#: taiga/projects/models.py:147 +#: taiga/projects/models.py:130 msgid "default severity" msgstr "預設嚴重性" -#: taiga/projects/models.py:151 +#: taiga/projects/models.py:134 msgid "default issue status" msgstr "預設問題狀態" -#: taiga/projects/models.py:155 +#: taiga/projects/models.py:138 msgid "default issue type" msgstr "預設議題類型" -#: taiga/projects/models.py:171 +#: taiga/projects/models.py:154 msgid "logo" msgstr "圖標" -#: taiga/projects/models.py:181 +#: taiga/projects/models.py:164 msgid "members" msgstr "成員" -#: taiga/projects/models.py:184 +#: taiga/projects/models.py:167 msgid "total of milestones" msgstr "全部里程碑" -#: taiga/projects/models.py:185 +#: taiga/projects/models.py:168 msgid "total story points" msgstr "全部故事點數" -#: taiga/projects/models.py:188 taiga/projects/models.py:697 +#: taiga/projects/models.py:171 taiga/projects/models.py:692 msgid "active backlog panel" msgstr "活躍的待辦任務優先表面板" -#: taiga/projects/models.py:190 taiga/projects/models.py:699 +#: taiga/projects/models.py:173 taiga/projects/models.py:694 msgid "active kanban panel" msgstr "活躍的看板式面板" -#: taiga/projects/models.py:192 taiga/projects/models.py:701 +#: taiga/projects/models.py:175 taiga/projects/models.py:696 msgid "active wiki panel" msgstr "活躍的維基面板" -#: taiga/projects/models.py:194 taiga/projects/models.py:703 +#: taiga/projects/models.py:177 taiga/projects/models.py:698 msgid "active issues panel" msgstr "活躍的問題面板" -#: taiga/projects/models.py:197 taiga/projects/models.py:706 +#: taiga/projects/models.py:180 taiga/projects/models.py:701 msgid "videoconference system" msgstr "視訊會議系統" -#: taiga/projects/models.py:199 taiga/projects/models.py:708 +#: taiga/projects/models.py:182 taiga/projects/models.py:703 msgid "videoconference extra data" msgstr "視訊會議額外資料" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:187 msgid "creation template" msgstr "創建模版" -#: taiga/projects/models.py:208 +#: taiga/projects/models.py:191 msgid "anonymous permissions" msgstr "匿名權限" -#: taiga/projects/models.py:212 +#: taiga/projects/models.py:195 msgid "user permissions" msgstr "使用者權限" -#: taiga/projects/models.py:215 +#: taiga/projects/models.py:198 msgid "is private" msgstr "私密" -#: taiga/projects/models.py:218 +#: taiga/projects/models.py:201 msgid "is featured" msgstr " 受矚目的" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:204 msgid "is looking for people" msgstr "正在找人" -#: taiga/projects/models.py:223 +#: taiga/projects/models.py:206 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:218 msgid "tags colors" msgstr "標籤顏色" -#: taiga/projects/models.py:239 taiga/projects/notifications/models.py:64 +#: taiga/projects/models.py:221 +msgid "project transfer token" +msgstr "" + +#: taiga/projects/models.py:225 +msgid "blocked code" +msgstr "" + +#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" msgstr "更新日期時間" -#: taiga/projects/models.py:242 taiga/projects/models.py:254 +#: taiga/projects/models.py:232 taiga/projects/models.py:244 #: taiga/projects/votes/models.py:29 msgid "count" msgstr "數量" -#: taiga/projects/models.py:245 +#: taiga/projects/models.py:235 msgid "fans last week" msgstr "上週粉絲" -#: taiga/projects/models.py:248 +#: taiga/projects/models.py:238 msgid "fans last month" msgstr "上個月粉絲" -#: taiga/projects/models.py:251 +#: taiga/projects/models.py:241 msgid "fans last year" msgstr "去年粉絲" -#: taiga/projects/models.py:257 +#: taiga/projects/models.py:247 msgid "activity last week" msgstr "上週活躍成員" -#: taiga/projects/models.py:260 +#: taiga/projects/models.py:250 msgid "activity last month" msgstr "上月活躍成員" -#: taiga/projects/models.py:263 +#: taiga/projects/models.py:253 msgid "activity last year" msgstr "去年活躍成員" -#: taiga/projects/models.py:466 +#: taiga/projects/models.py:461 msgid "modules config" msgstr "模組設定" -#: taiga/projects/models.py:485 +#: taiga/projects/models.py:480 msgid "is archived" msgstr "已歸檔" -#: taiga/projects/models.py:487 taiga/projects/models.py:549 -#: taiga/projects/models.py:582 taiga/projects/models.py:605 -#: taiga/projects/models.py:632 taiga/projects/models.py:663 -#: taiga/users/models.py:119 +#: taiga/projects/models.py:482 taiga/projects/models.py:544 +#: taiga/projects/models.py:577 taiga/projects/models.py:600 +#: taiga/projects/models.py:627 taiga/projects/models.py:658 +#: taiga/users/models.py:140 msgid "color" msgstr "顏色" -#: taiga/projects/models.py:489 +#: taiga/projects/models.py:484 msgid "work in progress limit" msgstr "工作進度限制" -#: taiga/projects/models.py:520 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:515 taiga/userstorage/models.py:32 msgid "value" msgstr "價值" -#: taiga/projects/models.py:694 +#: taiga/projects/models.py:689 msgid "default owner's role" msgstr "預設所有者角色" -#: taiga/projects/models.py:710 +#: taiga/projects/models.py:705 msgid "default options" msgstr "預設選項" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:706 msgid "us statuses" msgstr "我們狀況" -#: taiga/projects/models.py:712 taiga/projects/userstories/models.py:42 +#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42 #: taiga/projects/userstories/models.py:74 msgid "points" msgstr "點數" -#: taiga/projects/models.py:713 +#: taiga/projects/models.py:708 msgid "task statuses" msgstr "任務狀況" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:709 msgid "issue statuses" msgstr "問題狀況" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:710 msgid "issue types" msgstr "問題類型" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:711 msgid "priorities" msgstr "優先性" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:712 msgid "severities" msgstr "嚴重性" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:713 msgid "roles" msgstr "角色" @@ -1914,29 +1965,29 @@ msgstr "所有" msgid "None" msgstr "無" -#: taiga/projects/notifications/models.py:62 +#: taiga/projects/notifications/models.py:63 msgid "created date time" msgstr "創建日期時間" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:67 msgid "history entries" msgstr "歷史輸入" -#: taiga/projects/notifications/models.py:69 +#: taiga/projects/notifications/models.py:70 msgid "notify users" msgstr "通知用戶" -#: taiga/projects/notifications/models.py:91 #: taiga/projects/notifications/models.py:92 +#: taiga/projects/notifications/models.py:93 msgid "Watched" msgstr "已觀注" -#: taiga/projects/notifications/services.py:66 -#: taiga/projects/notifications/services.py:80 +#: taiga/projects/notifications/services.py:64 +#: taiga/projects/notifications/services.py:78 msgid "Notify exists for specified user and project" msgstr "通知特定使用者與專案退出" -#: taiga/projects/notifications/services.py:429 +#: taiga/projects/notifications/services.py:427 msgid "Invalid value for notify level" msgstr "通知水平的無效值" @@ -2682,54 +2733,63 @@ msgid "version" msgstr "版本" #: taiga/projects/permissions.py:40 -msgid "You can't leave the project if there are no more owners" -msgstr "如果專案無所有者,你將無法脫離該專案" +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" -#: taiga/projects/serializers.py:237 +#: taiga/projects/serializers.py:172 msgid "Email address is already taken" msgstr "電子郵件已使用" -#: taiga/projects/serializers.py:249 +#: taiga/projects/serializers.py:184 msgid "Invalid role for the project" msgstr "專案無效的角色" -#: taiga/projects/serializers.py:425 +#: taiga/projects/serializers.py:195 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/serializers.py:198 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/serializers.py:394 msgid "Default options" msgstr "預設選項" -#: taiga/projects/serializers.py:426 +#: taiga/projects/serializers.py:395 msgid "User story's statuses" msgstr "使用者故事狀態" -#: taiga/projects/serializers.py:427 +#: taiga/projects/serializers.py:396 msgid "Points" msgstr "點數" -#: taiga/projects/serializers.py:428 +#: taiga/projects/serializers.py:397 msgid "Task's statuses" msgstr "任務狀態" -#: taiga/projects/serializers.py:429 +#: taiga/projects/serializers.py:398 msgid "Issue's statuses" msgstr "問題狀態" -#: taiga/projects/serializers.py:430 +#: taiga/projects/serializers.py:399 msgid "Issue's types" msgstr "問題類型" -#: taiga/projects/serializers.py:431 +#: taiga/projects/serializers.py:400 msgid "Priorities" msgstr "優先性" -#: taiga/projects/serializers.py:432 +#: taiga/projects/serializers.py:401 msgid "Severities" msgstr "嚴重性" -#: taiga/projects/serializers.py:433 +#: taiga/projects/serializers.py:402 msgid "Roles" msgstr "角色" -#: taiga/projects/services/stats.py:198 +#: taiga/projects/services/stats.py:196 msgid "Future sprint" msgstr "未來之衝刺" @@ -2737,15 +2797,26 @@ msgstr "未來之衝刺" msgid "Project End" msgstr "專案結束" -#: taiga/projects/tasks/api.py:112 taiga/projects/tasks/api.py:121 +#: taiga/projects/services/transfer.py:61 +#: taiga/projects/services/transfer.py:68 +#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 +#: taiga/users/api.py:174 +msgid "Token is invalid" +msgstr "代號無效" + +#: taiga/projects/services/transfer.py:66 +msgid "Token has expired" +msgstr "" + +#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." msgstr "無權限更動此任務下的衝刺任務" -#: taiga/projects/tasks/api.py:115 +#: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." msgstr "無權限更動此務下的使用者故事" -#: taiga/projects/tasks/api.py:118 +#: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." msgstr "無權限更動此任務下的狀態" @@ -2910,6 +2981,236 @@ msgstr "" "\n" "[Taiga] 被加入專案 '%(project)s'\n" +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 msgid "Scrum" @@ -3147,15 +3448,15 @@ msgstr "產品所有人" msgid "Stakeholder" msgstr "利害關係人" -#: taiga/projects/userstories/api.py:162 +#: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." msgstr "無權限更動使用者故事的衝刺任務" -#: taiga/projects/userstories/api.py:166 +#: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." msgstr "無權限更動此使用者故事的狀態" -#: taiga/projects/userstories/api.py:260 +#: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "産生使用者故事 #{ref} - {subject}" @@ -3214,11 +3515,11 @@ msgstr "投票數" msgid "Vote" msgstr "投票 " -#: taiga/projects/wiki/api.py:67 +#: taiga/projects/wiki/api.py:70 msgid "'content' parameter is mandatory" msgstr "'content'參數為必要" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:73 msgid "'project_id' parameter is mandatory" msgstr "'project_id'參數為必要" @@ -3230,7 +3531,7 @@ msgstr "上次更改" msgid "href" msgstr "href" -#: taiga/timeline/signals.py:70 +#: taiga/timeline/signals.py:68 msgid "Check the history API for the exact diff" msgstr "檢查API過去資料以找出差異" @@ -3238,129 +3539,145 @@ msgstr "檢查API過去資料以找出差異" msgid "Personal info" msgstr "個人資訊" -#: taiga/users/admin.py:53 +#: taiga/users/admin.py:54 msgid "Permissions" msgstr "許可" -#: taiga/users/admin.py:54 +#: taiga/users/admin.py:55 +msgid "Restrictions" +msgstr "" + +#: taiga/users/admin.py:57 msgid "Important dates" msgstr "重要日期" -#: taiga/users/api.py:112 +#: taiga/users/api.py:113 msgid "Duplicated email" msgstr "複製電子郵件" -#: taiga/users/api.py:114 +#: taiga/users/api.py:115 msgid "Not valid email" msgstr "非有效電子郵性" -#: taiga/users/api.py:147 +#: taiga/users/api.py:148 msgid "Invalid username or email" msgstr "無效使用者或郵件" -#: taiga/users/api.py:156 +#: taiga/users/api.py:157 msgid "Mail sended successful!" msgstr "成功送出郵件" -#: taiga/users/api.py:168 taiga/users/api.py:173 -msgid "Token is invalid" -msgstr "代號無效" - -#: taiga/users/api.py:194 +#: taiga/users/api.py:195 msgid "Current password parameter needed" msgstr "需要目前密碼之參數" -#: taiga/users/api.py:197 +#: taiga/users/api.py:198 msgid "New password parameter needed" msgstr "需要新密碼參數" -#: taiga/users/api.py:200 +#: taiga/users/api.py:201 msgid "Invalid password length at least 6 charaters needed" msgstr "無效密碼長度,至少需6個字元" -#: taiga/users/api.py:203 +#: taiga/users/api.py:204 msgid "Invalid current password" msgstr "無效密碼" -#: taiga/users/api.py:250 taiga/users/api.py:256 +#: taiga/users/api.py:251 taiga/users/api.py:257 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "無效,請確認代號正確,之前是否曾使用過?" -#: taiga/users/api.py:283 taiga/users/api.py:291 taiga/users/api.py:294 +#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 msgid "Invalid, are you sure the token is correct?" msgstr "無效,請確認代號是否正確?" -#: taiga/users/models.py:75 +#: taiga/users/models.py:96 msgid "superuser status" msgstr "超級使用者狀態 " -#: taiga/users/models.py:76 +#: taiga/users/models.py:97 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "無經明確分派,即賦予該使用者所有權限," -#: taiga/users/models.py:106 +#: taiga/users/models.py:127 msgid "username" msgstr "使用者名稱" -#: taiga/users/models.py:107 +#: taiga/users/models.py:128 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "必填。最多30字元(可為數字,字母,符號....)" -#: taiga/users/models.py:110 +#: taiga/users/models.py:131 msgid "Enter a valid username." msgstr "輸入有效的使用者名稱 " -#: taiga/users/models.py:113 +#: taiga/users/models.py:134 msgid "active" msgstr "活躍" -#: taiga/users/models.py:114 +#: taiga/users/models.py:135 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "賦予該使用者活躍角色,以不選擇取代刪除帳戶功能。" -#: taiga/users/models.py:120 +#: taiga/users/models.py:141 msgid "biography" msgstr "自傳" -#: taiga/users/models.py:123 +#: taiga/users/models.py:144 msgid "photo" msgstr "照片" -#: taiga/users/models.py:124 +#: taiga/users/models.py:145 msgid "date joined" msgstr "加入日期" -#: taiga/users/models.py:126 +#: taiga/users/models.py:147 msgid "default language" msgstr "預設語言 " -#: taiga/users/models.py:128 +#: taiga/users/models.py:149 msgid "default theme" msgstr "預設主題" -#: taiga/users/models.py:130 +#: taiga/users/models.py:151 msgid "default timezone" msgstr "預設時區" -#: taiga/users/models.py:132 +#: taiga/users/models.py:153 msgid "colorize tags" msgstr "顏色標籤" -#: taiga/users/models.py:137 +#: taiga/users/models.py:158 msgid "email token" msgstr "電子郵件符號 " -#: taiga/users/models.py:139 +#: taiga/users/models.py:160 msgid "new email address" msgstr "新電子郵件地址" -#: taiga/users/models.py:256 +#: taiga/users/models.py:167 +msgid "max number of owned private projects" +msgstr "" + +#: taiga/users/models.py:170 +msgid "max number of owned public projects" +msgstr "" + +#: taiga/users/models.py:173 +msgid "max number of memberships for each owned private project" +msgstr "" + +#: taiga/users/models.py:177 +msgid "max number of memberships for each owned public project" +msgstr "" + +#: taiga/users/models.py:294 msgid "permissions" msgstr "許可" @@ -3372,10 +3689,26 @@ msgstr "無效" msgid "Invalid username. Try with a different one." msgstr "無效使用者名稱,請重試其它名稱 " -#: taiga/users/services.py:52 taiga/users/services.py:69 +#: taiga/users/services.py:53 taiga/users/services.py:70 msgid "Username or password does not matches user." msgstr "用戶名稱與密碼不符" +#: taiga/users/services.py:602 +msgid "You can't have more private projects" +msgstr "" + +#: taiga/users/services.py:612 +msgid "You can't have more public projects" +msgstr "" + +#: taiga/users/services.py:625 +msgid "You have reached your current limit of memberships for private projects" +msgstr "" + +#: taiga/users/services.py:634 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" diff --git a/taiga/mdrender/__init__.py b/taiga/mdrender/__init__.py index 11dbddfa..e69de29b 100644 --- a/taiga/mdrender/__init__.py +++ b/taiga/mdrender/__init__.py @@ -1,18 +0,0 @@ -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -from .service import * diff --git a/taiga/mdrender/extensions/mentions.py b/taiga/mdrender/extensions/mentions.py index 2dcea03a..683250d2 100644 --- a/taiga/mdrender/extensions/mentions.py +++ b/taiga/mdrender/extensions/mentions.py @@ -22,13 +22,12 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from django.contrib.auth import get_user_model from markdown.extensions import Extension from markdown.inlinepatterns import Pattern from markdown.util import etree, AtomicString -from taiga.users.models import User - class MentionsExtension(Extension): def extendMarkdown(self, md, md_globals): @@ -43,8 +42,8 @@ class MentionsPattern(Pattern): username = m.group(3) try: - user = User.objects.get(username=username) - except User.DoesNotExist: + user = get_user_model().objects.get(username=username) + except get_user_model().DoesNotExist: return "@{}".format(username) url = "/profile/{}".format(username) diff --git a/taiga/mdrender/extensions/wikilinks.py b/taiga/mdrender/extensions/wikilinks.py index d3ac7030..61c36d5c 100644 --- a/taiga/mdrender/extensions/wikilinks.py +++ b/taiga/mdrender/extensions/wikilinks.py @@ -7,85 +7,85 @@ # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -# +# # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -from markdown import Extension -from markdown.inlinepatterns import Pattern -from markdown.treeprocessors import Treeprocessor - -from markdown.util import etree - -from taiga.front.templatetags.functions import resolve -from taiga.base.utils.slug import slugify - -import re - - -class WikiLinkExtension(Extension): - def __init__(self, project, *args, **kwargs): - self.project = project - return super().__init__(*args, **kwargs) - - def extendMarkdown(self, md, md_globals): - WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]" - md.inlinePatterns.add("wikilinks", - WikiLinksPattern(md, WIKILINK_RE, self.project), - " . -from .permissions import OWNERS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from django.apps import apps def _get_user_project_membership(user, project): - Membership = apps.get_model("projects", "Membership") if user.is_anonymous(): return None @@ -39,10 +38,17 @@ def _get_object_project(obj): def is_project_owner(user, obj): - """ - The owner attribute of a project is just an historical reference - """ + project = _get_object_project(obj) + if project is None: + return False + if user.id == project.owner.id: + return True + + return False + + +def is_project_admin(user, obj): if user.is_superuser: return True @@ -51,7 +57,7 @@ def is_project_owner(user, obj): return False membership = _get_user_project_membership(user, project) - if membership and membership.is_owner: + if membership and membership.is_admin: return True return False @@ -79,43 +85,41 @@ def _get_membership_permissions(membership): def get_user_project_permissions(user, project): membership = _get_user_project_membership(user, project) if user.is_superuser: - owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS)) + admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS)) anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) elif membership: - if membership.is_owner: - owner_permissions = list(map(lambda perm: perm[0], OWNERS_PERMISSIONS)) + if membership.is_admin: + admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) else: - owner_permissions = [] + admins_permissions = [] members_permissions = [] members_permissions = members_permissions + _get_membership_permissions(membership) public_permissions = project.public_permissions if project.public_permissions is not None else [] anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] elif user.is_authenticated(): - owner_permissions = [] + admins_permissions = [] members_permissions = [] public_permissions = project.public_permissions if project.public_permissions is not None else [] anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] else: - owner_permissions = [] + admins_permissions = [] members_permissions = [] public_permissions = [] anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] - return set(owner_permissions + members_permissions + public_permissions + anon_permissions) + return set(admins_permissions + members_permissions + public_permissions + anon_permissions) def set_base_permissions_for_project(project): if project.is_private: project.anon_permissions = [] project.public_permissions = [] - else: - """ - If a project is public anonymous and registered users should have at least visualization permissions - """ + # If a project is public anonymous and registered users should have at + # least visualization permissions. anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) project.anon_permissions = list(set((project.anon_permissions or []) + anon_permissions)) project.public_permissions = list(set((project.public_permissions or []) + anon_permissions)) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index c0b122f3..d32115c4 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -26,6 +26,7 @@ from django.db.models.functions import Coalesce from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from django.utils import timezone +from django.http import Http404 from taiga.base import filters from taiga.base import response @@ -33,6 +34,7 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin from taiga.base.api.permissions import AllowAnyPermission from taiga.base.api.utils import get_object_or_404 from taiga.base.utils.slug import slugify_uniquely @@ -50,6 +52,7 @@ from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin from taiga.permissions import service as permissions_service +from taiga.users import services as users_service from . import filters as project_filters from . import models @@ -61,7 +64,9 @@ from . import services ###################################################### ## Project ###################################################### -class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet): +class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, + BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet): + queryset = models.Project.objects.all() serializer_class = serializers.ProjectDetailSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer @@ -88,6 +93,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) "total_activity_last_month", "total_activity_last_year") + def is_blocked(self, obj): + return obj.blocked_code is not None + def _get_order_by_field_name(self): order_by_query_param = project_filters.CanViewProjectObjFilterBackend.order_by_query_param order_by = self.request.QUERY_PARAMS.get(order_by_query_param, None) @@ -97,9 +105,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) def get_queryset(self): qs = super().get_queryset() + qs = qs.select_related("owner") # Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries) # so we add some custom prefetching qs = qs.prefetch_related("members") + qs = qs.prefetch_related("memberships") qs = qs.prefetch_related(Prefetch("notify_policies", NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies")) @@ -137,7 +147,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) else: project = self.get_object() - if permissions_service.is_project_owner(self.request.user, project): + if permissions_service.is_project_admin(self.request.user, project): serializer_class = self.admin_serializer_class return serializer_class @@ -158,6 +168,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) except Exception: raise exc.WrongArguments(_("Invalid image format")) + self.pre_conditions_on_save(self.object) + self.object.logo = logo self.object.save(update_fields=["logo"]) @@ -171,7 +183,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) """ self.object = get_object_or_404(self.get_queryset(), **kwargs) self.check_permissions(request, "remove_logo", self.object) - + self.pre_conditions_on_save(self.object) self.object.logo = None self.object.save(update_fields=["logo"]) @@ -182,6 +194,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) def watch(self, request, pk=None): project = self.get_object() self.check_permissions(request, "watch", project) + self.pre_conditions_on_save(project) notify_level = request.DATA.get("notify_level", NotifyLevel.involved) project.add_watcher(self.request.user, notify_level=notify_level) return response.Ok() @@ -190,6 +203,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) def unwatch(self, request, pk=None): project = self.get_object() self.check_permissions(request, "unwatch", project) + self.pre_conditions_on_save(project) user = self.request.user project.remove_watcher(user) return response.Ok() @@ -207,77 +221,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) services.update_projects_order_in_bulk(data, "user_order", request.user) return response.NoContent(data=None) - @list_route(methods=["GET"]) - def by_slug(self, request): - slug = request.QUERY_PARAMS.get("slug", None) - project = get_object_or_404(models.Project, slug=slug) - return self.retrieve(request, pk=project.pk) - - @detail_route(methods=["GET", "PATCH"]) - def modules(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, 'modules', project) - modules_config = services.get_modules_config(project) - - if request.method == "GET": - return response.Ok(modules_config.config) - - else: - modules_config.config.update(request.DATA) - modules_config.save() - return response.NoContent() - - @detail_route(methods=["GET"]) - def stats(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "stats", project) - return response.Ok(services.get_stats_for_project(project)) - - def _regenerate_csv_uuid(self, project, field): - uuid_value = uuid.uuid4().hex - setattr(project, field, uuid_value) - project.save() - return uuid_value - - @detail_route(methods=["POST"]) - def regenerate_userstories_csv_uuid(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "regenerate_userstories_csv_uuid", project) - data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")} - return response.Ok(data) - - @detail_route(methods=["POST"]) - def regenerate_issues_csv_uuid(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "regenerate_issues_csv_uuid", project) - data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} - return response.Ok(data) - - @detail_route(methods=["POST"]) - def regenerate_tasks_csv_uuid(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "regenerate_tasks_csv_uuid", project) - data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")} - return response.Ok(data) - - @detail_route(methods=["GET"]) - def member_stats(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "member_stats", project) - return response.Ok(services.get_member_stats_for_project(project)) - - @detail_route(methods=["GET"]) - def issues_stats(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "issues_stats", project) - return response.Ok(services.get_stats_for_project_issues(project)) - - @detail_route(methods=["GET"]) - def tags_colors(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "tags_colors", project) - return response.Ok(dict(project.tags_colors)) - @detail_route(methods=["POST"]) def create_template(self, request, **kwargs): template_name = request.DATA.get('template_name', None) @@ -305,13 +248,161 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) template.save() return response.Created(serializers.ProjectTemplateSerializer(template).data) - @detail_route(methods=['post']) + @detail_route(methods=['POST']) def leave(self, request, pk=None): project = self.get_object() self.check_permissions(request, 'leave', project) + self.pre_conditions_on_save(project) services.remove_user_from_project(request.user, project) return response.Ok() + @detail_route(methods=["POST"]) + def regenerate_userstories_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_userstories_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")} + return response.Ok(data) + + @detail_route(methods=["POST"]) + def regenerate_issues_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_issues_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} + return response.Ok(data) + + @detail_route(methods=["POST"]) + def regenerate_tasks_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_tasks_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")} + return response.Ok(data) + + @list_route(methods=["GET"]) + def by_slug(self, request): + slug = request.QUERY_PARAMS.get("slug", None) + project = get_object_or_404(models.Project, slug=slug) + return self.retrieve(request, pk=project.pk) + + @detail_route(methods=["GET", "PATCH"]) + def modules(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'modules', project) + modules_config = services.get_modules_config(project) + + if request.method == "GET": + return response.Ok(modules_config.config) + + else: + self.pre_conditions_on_save(project) + modules_config.config.update(request.DATA) + modules_config.save() + return response.NoContent() + + @detail_route(methods=["GET"]) + def stats(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "stats", project) + return response.Ok(services.get_stats_for_project(project)) + + def _regenerate_csv_uuid(self, project, field): + uuid_value = uuid.uuid4().hex + setattr(project, field, uuid_value) + project.save() + return uuid_value + + @detail_route(methods=["GET"]) + def member_stats(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "member_stats", project) + return response.Ok(services.get_member_stats_for_project(project)) + + @detail_route(methods=["GET"]) + def issues_stats(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "issues_stats", project) + return response.Ok(services.get_stats_for_project_issues(project)) + + @detail_route(methods=["GET"]) + def tags_colors(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "tags_colors", project) + return response.Ok(dict(project.tags_colors)) + + @detail_route(methods=["POST"]) + def transfer_validate_token(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "transfer_validate_token", project) + token = request.DATA.get('token', None) + services.transfer.validate_project_transfer_token(token, project, request.user) + return response.Ok() + + @detail_route(methods=["POST"]) + def transfer_request(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "transfer_request", project) + services.request_project_transfer(project, request.user) + return response.Ok() + + @detail_route(methods=['post']) + def transfer_start(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "transfer_start", project) + + user_id = request.DATA.get('user', None) + if user_id is None: + raise exc.WrongArguments(_("Invalid user id")) + + user_model = apps.get_model("users", "User") + try: + user = user_model.objects.get(id=user_id) + except user_model.DoesNotExist: + return response.BadRequest(_("The user doesn't exist")) + + # Check the user is a membership from the project + if not project.memberships.filter(user=user).exists(): + return response.BadRequest(_("The user must be already a project member")) + + reason = request.DATA.get('reason', None) + transfer_token = services.start_project_transfer(project, user, reason) + return response.Ok() + + @detail_route(methods=["POST"]) + def transfer_accept(self, request, pk=None): + token = request.DATA.get('token', None) + if token is None: + raise exc.WrongArguments(_("Invalid token")) + + project = self.get_object() + self.check_permissions(request, "transfer_accept", project) + + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + request.user, + project, + ) + if not enough_slots: + members = project.memberships.count() + raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error) + + reason = request.DATA.get('reason', None) + services.accept_project_transfer(project, request.user, token, reason) + return response.Ok() + + @detail_route(methods=["POST"]) + def transfer_reject(self, request, pk=None): + token = request.DATA.get('token', None) + if token is None: + raise exc.WrongArguments(_("Invalid token")) + + project = self.get_object() + self.check_permissions(request, "transfer_reject", project) + + reason = request.DATA.get('reason', None) + services.reject_project_transfer(project, request.user, token, reason) + return response.Ok() + def _set_base_permissions(self, obj): update_permissions = False if not obj.id: @@ -329,9 +420,15 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) def pre_save(self, obj): if not obj.id: obj.owner = self.request.user - # TODO REFACTOR THIS obj.template = self.request.QUERY_PARAMS.get('template', None) + # Validate if the owner have enought slots to create or update the project + # TODO: Move to the ProjectAdminSerializer + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(obj.owner, obj) + if not enough_slots: + members = max(obj.memberships.count(), 1) + raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error) + self._set_base_permissions(obj) super().pre_save(obj) @@ -342,10 +439,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet) if obj is None: raise Http404 - obj.delete_related_content() - self.pre_delete(obj) self.pre_conditions_on_delete(obj) + obj.delete_related_content() obj.delete() self.post_delete(obj) return response.NoContent() @@ -365,7 +461,9 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): ## Custom values for selectors ###################################################### -class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.Points serializer_class = serializers.PointsSerializer permission_classes = (permissions.PointsPermission,) @@ -379,7 +477,9 @@ class PointsViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): move_on_destroy_project_default_field = "default_points" -class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.UserStoryStatus serializer_class = serializers.UserStoryStatusSerializer permission_classes = (permissions.UserStoryStatusPermission,) @@ -393,7 +493,9 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrd move_on_destroy_project_default_field = "default_us_status" -class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.TaskStatus serializer_class = serializers.TaskStatusSerializer permission_classes = (permissions.TaskStatusPermission,) @@ -407,7 +509,9 @@ class TaskStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMix move_on_destroy_project_default_field = "default_task_status" -class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): + model = models.Severity serializer_class = serializers.SeveritySerializer permission_classes = (permissions.SeverityPermission,) @@ -421,7 +525,8 @@ class SeverityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin move_on_destroy_project_default_field = "default_severity" -class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): model = models.Priority serializer_class = serializers.PrioritySerializer permission_classes = (permissions.PriorityPermission,) @@ -435,7 +540,8 @@ class PriorityViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin move_on_destroy_project_default_field = "default_priority" -class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): model = models.IssueType serializer_class = serializers.IssueTypeSerializer permission_classes = (permissions.IssueTypePermission,) @@ -449,7 +555,8 @@ class IssueTypeViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixi move_on_destroy_project_default_field = "default_issue_type" -class IssueStatusViewSet(MoveOnDestroyMixin, ModelCrudViewSet, BulkUpdateOrderMixin): +class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): model = models.IssueStatus serializer_class = serializers.IssueStatusSerializer permission_classes = (permissions.IssueStatusPermission,) @@ -480,7 +587,7 @@ class ProjectTemplateViewSet(ModelCrudViewSet): ## Members & Invitations ###################################################### -class MembershipViewSet(ModelCrudViewSet): +class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Membership admin_serializer_class = serializers.MembershipAdminSerializer serializer_class = serializers.MembershipSerializer @@ -490,17 +597,17 @@ class MembershipViewSet(ModelCrudViewSet): def get_serializer_class(self): use_admin_serializer = False - + if self.action == "create": use_admin_serializer = True if self.action == "retrieve": - use_admin_serializer = permissions_service.is_project_owner(self.request.user, self.object.project) + use_admin_serializer = permissions_service.is_project_admin(self.request.user, self.object.project) project_id = self.request.QUERY_PARAMS.get("project", None) if self.action == "list" and project_id is not None: project = get_object_or_404(models.Project, pk=project_id) - use_admin_serializer = permissions_service.is_project_owner(self.request.user, project) + use_admin_serializer = permissions_service.is_project_admin(self.request.user, project) if use_admin_serializer: return self.admin_serializer_class @@ -518,10 +625,22 @@ class MembershipViewSet(ModelCrudViewSet): project = models.Project.objects.get(id=data["project_id"]) invitation_extra_text = data.get("invitation_extra_text", None) self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) # TODO: this should be moved to main exception handler instead # of handling explicit exception catchin here. + if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list): + members = len(data["bulk_memberships"]) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + project.owner, + project, + members + ) + if not enough_slots: + raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error) + try: members = services.create_members_in_bulk(data["bulk_memberships"], project=project, @@ -539,15 +658,26 @@ class MembershipViewSet(ModelCrudViewSet): invitation = self.get_object() self.check_permissions(request, 'resend_invitation', invitation.project) + self.pre_conditions_on_save(invitation) services.send_invitation(invitation=invitation) return response.NoContent() def pre_delete(self, obj): if obj.user is not None and not services.can_user_leave_project(obj.user, obj.project): - raise exc.BadRequest(_("At least one of the user must be an active admin")) + raise exc.BadRequest(_("The project must have an owner and at least one of the users must be an active admin")) def pre_save(self, obj): + if not obj.id: + members = 1 + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + self.request.user, + obj.project, + members + ) + if not enough_slots: + raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error) + if not obj.token: obj.token = str(uuid.uuid1()) diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py index 841b81d7..c8c56bb3 100644 --- a/taiga/projects/apps.py +++ b/taiga/projects/apps.py @@ -19,12 +19,11 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from . import signals as handlers - ## Project Signals def connect_projects_signals(): + from . import signals as handlers # On project object is created apply template. signals.post_save.connect(handlers.project_post_save, sender=apps.get_model("projects", "Project"), @@ -51,6 +50,7 @@ def disconnect_projects_signals(): ## Memberships Signals def connect_memberships_signals(): + from . import signals as handlers # On membership object is deleted, update role-points relation. signals.pre_delete.connect(handlers.membership_post_delete, sender=apps.get_model("projects", "Membership"), @@ -71,6 +71,7 @@ def disconnect_memberships_signals(): ## US Statuses Signals def connect_us_status_signals(): + from . import signals as handlers signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status, sender=apps.get_model("projects", "UserStoryStatus"), dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status") @@ -85,6 +86,7 @@ def disconnect_us_status_signals(): ## Tasks Statuses Signals def connect_task_status_signals(): + from . import signals as handlers signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status, sender=apps.get_model("projects", "TaskStatus"), dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status") diff --git a/taiga/projects/attachments/admin.py b/taiga/projects/attachments/admin.py index b64613ed..ecfa6fed 100644 --- a/taiga/projects/attachments/admin.py +++ b/taiga/projects/attachments/admin.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.contrib import admin -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.admin import GenericTabularInline from . import models @@ -38,7 +38,7 @@ class AttachmentAdmin(admin.ModelAdmin): return super().formfield_for_foreignkey(db_field, request, **kwargs) -class AttachmentInline(generic.GenericTabularInline): +class AttachmentInline(GenericTabularInline): model = models.Attachment fields = ("attached_file", "owner") extra = 0 diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 65ad55b3..481021ed 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -25,6 +25,7 @@ from django.contrib.contenttypes.models import ContentType from taiga.base import filters from taiga.base import exceptions as exc from taiga.base.api import ModelCrudViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 from taiga.projects.notifications.mixins import WatchedResourceMixin @@ -35,7 +36,9 @@ from . import serializers from . import models -class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): +class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, + BlockedByProjectMixin, ModelCrudViewSet): + model = models.Attachment serializer_class = serializers.AttachmentSerializer filter_fields = ["project", "object_id"] diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index 40cbf611..daec4a2c 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -20,7 +20,7 @@ import hashlib from django.db import models from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.utils.text import get_valid_filename @@ -42,7 +42,7 @@ class Attachment(models.Model): verbose_name=_("content type")) object_id = models.PositiveIntegerField(null=False, blank=False, verbose_name=_("object id")) - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), default=timezone.now) diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py index 2d929ec7..15234a6d 100644 --- a/taiga/projects/choices.py +++ b/taiga/projects/choices.py @@ -24,3 +24,12 @@ VIDEOCONFERENCES_CHOICES = ( ("custom", _("Custom")), ("talky", _("Talky")), ) + +BLOCKED_BY_NONPAYMENT = "blocked-by-nonpayment" +BLOCKED_BY_STAFF = "blocked-by-staff" +BLOCKED_BY_OWNER_LEAVING = "blocked-by-owner-leaving" +BLOCKING_CODES = [ + (BLOCKED_BY_NONPAYMENT, _("This project is blocked due to payment failure")), + (BLOCKED_BY_STAFF, _("This project is blocked by admin staff")), + (BLOCKED_BY_OWNER_LEAVING, _("This project is blocked because the owner left")) +] diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index 3224d617..a11d6e31 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _ from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelUpdateRetrieveViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base import exceptions as exc from taiga.base import filters from taiga.base import response @@ -38,7 +39,7 @@ from . import services # Custom Attribute ViewSets ####################################################### -class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): +class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.UserStoryCustomAttribute serializer_class = serializers.UserStoryCustomAttributeSerializer permission_classes = (permissions.UserStoryCustomAttributePermission,) @@ -49,7 +50,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order -class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): +class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.TaskCustomAttribute serializer_class = serializers.TaskCustomAttributeSerializer permission_classes = (permissions.TaskCustomAttributePermission,) @@ -60,7 +61,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): bulk_update_order_action = services.bulk_update_task_custom_attribute_order -class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): +class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.IssueCustomAttribute serializer_class = serializers.IssueCustomAttributeSerializer permission_classes = (permissions.IssueCustomAttributePermission,) @@ -76,7 +77,7 @@ class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet): ####################################################### class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - ModelUpdateRetrieveViewSet): + BlockedByProjectMixin, ModelUpdateRetrieveViewSet): def get_object_for_snapshot(self, obj): return getattr(obj, self.content_object) diff --git a/taiga/projects/custom_attributes/choices.py b/taiga/projects/custom_attributes/choices.py index dd6c15e9..fadcc788 100644 --- a/taiga/projects/custom_attributes/choices.py +++ b/taiga/projects/custom_attributes/choices.py @@ -21,9 +21,11 @@ from django.utils.translation import ugettext_lazy as _ TEXT_TYPE = "text" MULTILINE_TYPE = "multiline" DATE_TYPE = "date" +URL_TYPE = "url" TYPES_CHOICES = ( (TEXT_TYPE, _("Text")), (MULTILINE_TYPE, _("Multi-Line Text")), - (DATE_TYPE, _("Date")) + (DATE_TYPE, _("Date")), + (URL_TYPE, _("Url")) ) diff --git a/taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py b/taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py new file mode 100644 index 00000000..08acc42f --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0007_auto_20160208_1751.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_attributes', '0006_auto_20151014_1645'), + ] + + operations = [ + migrations.AlterField( + model_name='issuecustomattribute', + name='type', + field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]), + ), + migrations.AlterField( + model_name='taskcustomattribute', + name='type', + field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]), + ), + migrations.AlterField( + model_name='userstorycustomattribute', + name='type', + field=models.CharField(default='text', max_length=16, verbose_name='type', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')]), + ), + ] diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py index 96fe120d..766f9d42 100644 --- a/taiga/projects/custom_attributes/permissions.py +++ b/taiga/projects/custom_attributes/permissions.py @@ -17,7 +17,7 @@ from taiga.base.api.permissions import TaigaResourcePermission from taiga.base.api.permissions import HasProjectPerm -from taiga.base.api.permissions import IsProjectOwner +from taiga.base.api.permissions import IsProjectAdmin from taiga.base.api.permissions import AllowAny from taiga.base.api.permissions import IsSuperUser @@ -27,39 +27,39 @@ from taiga.base.api.permissions import IsSuperUser ####################################################### class UserStoryCustomAttributePermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() class TaskCustomAttributePermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() class IssueCustomAttributePermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() ###################################################### @@ -67,7 +67,7 @@ class IssueCustomAttributePermission(TaigaResourcePermission): ####################################################### class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_us') update_perms = HasProjectPerm('modify_us') @@ -75,7 +75,7 @@ class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission): class TaskCustomAttributesValuesPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_tasks') update_perms = HasProjectPerm('modify_task') @@ -83,7 +83,7 @@ class TaskCustomAttributesValuesPermission(TaigaResourcePermission): class IssueCustomAttributesValuesPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_issues') update_perms = HasProjectPerm('modify_issue') diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py index 5868a6be..f01ed179 100644 --- a/taiga/projects/filters.py +++ b/taiga/projects/filters.py @@ -37,7 +37,8 @@ class DiscoverModeFilterBackend(FilterBackend): if discover_mode: # discover_mode enabled - qs = qs.filter(anon_permissions__contains=["view_project"]) + qs = qs.filter(anon_permissions__contains=["view_project"], + blocked_code__isnull=True) return super().filter_queryset(request, qs.distinct(), view) @@ -70,7 +71,7 @@ class CanViewProjectObjFilterBackend(FilterBackend): if project_id: memberships_qs = memberships_qs.filter(project_id=project_id) memberships_qs = memberships_qs.filter(Q(role__permissions__contains=['view_project']) | - Q(is_owner=True)) + Q(is_admin=True)) projects_list = [membership.project_id for membership in memberships_qs] diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 07de4bfc..34f4139d 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -19,10 +19,10 @@ from contextlib import suppress from functools import partial from django.apps import apps +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from taiga.base.utils.urls import get_absolute_url from taiga.base.utils.iterators import as_tuple from taiga.base.utils.iterators import as_dict from taiga.mdrender.service import render as mdrender @@ -49,7 +49,7 @@ def _get_generic_values(ids:tuple, *, typename=None, attr:str="name") -> tuple: @as_dict def _get_users_values(ids:set) -> dict: - user_model = apps.get_model("users", "User") + user_model = get_user_model() ids = filter(lambda x: x is not None, ids) qs = user_model.objects.filter(pk__in=tuple(ids)) diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 24526235..d440900c 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -14,12 +14,10 @@ import uuid -from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.db import models -from django.apps import apps +from django.contrib.auth import get_user_model from django.utils.functional import cached_property -from django.conf import settings from django_pgjson.fields import JsonField from taiga.mdrender.service import get_diff_of_htmls @@ -96,7 +94,7 @@ class HistoryEntry(models.Model): @cached_property def owner(self): pk = self.user["pk"] - model = apps.get_model("users", "User") + model = get_user_model() try: return model.objects.get(pk=pk) except model.DoesNotExist: diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index fd139826..015ac22c 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -16,10 +16,10 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectOwner, AllowAny, + IsProjectAdmin, AllowAny, IsObjectOwner, PermissionComponent) -from taiga.permissions.service import is_project_owner +from taiga.permissions.service import is_project_admin from taiga.projects.history.services import get_model_from_key, get_pk_from_key @@ -38,7 +38,7 @@ class IsCommentProjectOwner(PermissionComponent): model = get_model_from_key(obj.key) pk = get_pk_from_key(obj.key) project = model.objects.get(pk=pk) - return is_project_owner(request.user, project) + return is_project_admin(request.user, project) class UserStoryHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 59583ee5..c0ade2f4 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -16,24 +16,21 @@ # along with this program. If not, see . from django.utils.translation import ugettext as _ -from django.db.models import Q from django.http import HttpResponse from taiga.base import filters from taiga.base import exceptions as exc from taiga.base import response -from taiga.base.decorators import detail_route, list_route +from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 -from taiga.users.models import User - from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType -from taiga.projects.milestones.models import Milestone from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -43,7 +40,7 @@ from . import serializers class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - ModelCrudViewSet): + BlockedByProjectMixin, ModelCrudViewSet): queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) filter_backends = (filters.CanViewIssuesFilterBackend, @@ -157,8 +154,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W super().pre_save(obj) def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - if obj.milestone and obj.milestone.project != obj.project: raise exc.PermissionDenied(_("You don't have permissions to set this sprint " "to this issue.")) @@ -179,6 +174,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W raise exc.PermissionDenied(_("You don't have permissions to set this type " "to this issue.")) + super().pre_conditions_on_save(obj) + @list_route(methods=["GET"]) def by_ref(self, request): ref = request.QUERY_PARAMS.get("ref", None) @@ -232,6 +229,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W data = serializer.data project = Project.objects.get(pk=data["project_id"]) self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + issues = services.create_issues_in_bulk( data["bulk_issues"], project=project, owner=request.user, status=project.default_issue_status, severity=project.default_severity, diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index 7c9352f7..26d58b18 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -19,12 +19,11 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from taiga.projects import signals as generic_handlers -from taiga.projects.custom_attributes import signals as custom_attributes_handlers -from . import signals as handlers - def connect_issues_signals(): + from taiga.projects import signals as generic_handlers + from . import signals as handlers + # Finished date signals.pre_save.connect(handlers.set_finished_date_when_edit_issue, sender=apps.get_model("issues", "Issue"), @@ -43,6 +42,8 @@ def connect_issues_signals(): def connect_issues_custom_attributes_signals(): + from taiga.projects.custom_attributes import signals as custom_attributes_handlers + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue, sender=apps.get_model("issues", "Issue"), dispatch_uid="create_custom_attribute_value_when_create_issue") diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 3faac676..df11f671 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils import timezone from django.dispatch import receiver @@ -63,7 +63,7 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None, related_name="issues_assigned_to_me", verbose_name=_("assigned to")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") external_reference = TextArrayField(default=None, verbose_name=_("external reference")) _importing = None diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index 07e17201..291dede7 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -17,12 +17,12 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectOwner, PermissionComponent, + IsProjectAdmin, PermissionComponent, AllowAny, IsAuthenticated, IsSuperUser) class IssuePermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_issues') create_perms = HasProjectPerm('add_issue') @@ -49,14 +49,14 @@ class HasIssueIdUrlParam(PermissionComponent): class IssueVotersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_issues') list_perms = HasProjectPerm('view_issues') class IssueWatchersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_issues') list_perms = HasProjectPerm('view_issues') diff --git a/taiga/projects/likes/mixins/viewsets.py b/taiga/projects/likes/mixins/viewsets.py index 0b1b1831..03bf8987 100644 --- a/taiga/projects/likes/mixins/viewsets.py +++ b/taiga/projects/likes/mixins/viewsets.py @@ -27,10 +27,15 @@ from taiga.projects.likes import services class LikedResourceMixin: + """ + NOTE:the classes using this mixing must have a method: + def pre_conditions_on_save(self, obj) + """ @detail_route(methods=["POST"]) def like(self, request, pk=None): obj = self.get_object() self.check_permissions(request, "like", obj) + self.pre_conditions_on_save(obj) services.add_like(obj, user=request.user) return response.Ok() @@ -39,6 +44,7 @@ class LikedResourceMixin: def unlike(self, request, pk=None): obj = self.get_object() self.check_permissions(request, "unlike", obj) + self.pre_conditions_on_save(obj) services.remove_like(obj, user=request.user) return response.Ok() diff --git a/taiga/projects/likes/models.py b/taiga/projects/likes/models.py index d5c4119f..078f48e3 100644 --- a/taiga/projects/likes/models.py +++ b/taiga/projects/likes/models.py @@ -17,7 +17,7 @@ # along with this program. If not, see . from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _ class Like(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, related_name="likes", verbose_name=_("user")) created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, diff --git a/taiga/projects/likes/serializers.py b/taiga/projects/likes/serializers.py index ce321f24..7d28f8f8 100644 --- a/taiga/projects/likes/serializers.py +++ b/taiga/projects/likes/serializers.py @@ -16,16 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api import serializers -from taiga.base.fields import TagsField +from django.contrib.auth import get_user_model -from taiga.users.models import User -from taiga.users.services import get_photo_or_gravatar_url +from taiga.base.api import serializers class FanSerializer(serializers.ModelSerializer): full_name = serializers.CharField(source='get_full_name', required=False) class Meta: - model = User + model = get_user_model() fields = ('id', 'username', 'full_name') diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 2a4e538a..f5c7b6ea 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -30,18 +30,18 @@ from sampledatahelper.helper import SampleDataHelper from taiga.users.models import * from taiga.permissions.permissions import ANON_PERMISSIONS +from taiga.projects.choices import BLOCKED_BY_STAFF from taiga.projects.models import * from taiga.projects.milestones.models import * from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.services.stats import get_stats_for_project - from taiga.projects.userstories.models import * from taiga.projects.tasks.models import * from taiga.projects.issues.models import * from taiga.projects.wiki.models import * from taiga.projects.attachments.models import * from taiga.projects.custom_attributes.models import * -from taiga.projects.custom_attributes.choices import TYPES_CHOICES, TEXT_TYPE, MULTILINE_TYPE, DATE_TYPE +from taiga.projects.custom_attributes.choices import TYPES_CHOICES, TEXT_TYPE, MULTILINE_TYPE, DATE_TYPE, URL_TYPE from taiga.projects.history.services import take_snapshot from taiga.projects.likes.services import add_like from taiga.projects.votes.services import add_vote @@ -94,11 +94,18 @@ SUBJECT_CHOICES = [ "Support for bulk actions", "Migrate to Python 3 and milk a beautiful cow"] +URL_CHOICES = [ + "https://taiga.io", + "https://blog.taiga.io", + "https://tree.taiga.io", + "https://tribe.taiga.io"] + BASE_USERS = getattr(settings, "SAMPLE_DATA_BASE_USERS", {}) NUM_USERS = getattr(settings, "SAMPLE_DATA_NUM_USERS", 10) NUM_INVITATIONS =getattr(settings, "SAMPLE_DATA_NUM_INVITATIONS", 2) NUM_PROJECTS =getattr(settings, "SAMPLE_DATA_NUM_PROJECTS", 4) NUM_EMPTY_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_EMPTY_PROJECTS", 2) +NUM_BLOCKED_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_BLOCKED_PROJECTS", 1) NUM_MILESTONES = getattr(settings, "SAMPLE_DATA_NUM_MILESTONES", (1, 5)) NUM_USS = getattr(settings, "SAMPLE_DATA_NUM_USS", (3, 7)) NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5)) @@ -132,8 +139,19 @@ class Command(BaseCommand): self.users.append(self.create_user(counter=x)) # create project - for x in range(NUM_PROJECTS + NUM_EMPTY_PROJECTS): - project = self.create_project(x, is_private=(x in [2, 4] or self.sd.boolean())) + projects_range = range(NUM_PROJECTS + NUM_EMPTY_PROJECTS + NUM_BLOCKED_PROJECTS) + empty_projects_range = range(NUM_PROJECTS, NUM_PROJECTS + NUM_EMPTY_PROJECTS ) + blocked_projects_range = range( + NUM_PROJECTS + NUM_EMPTY_PROJECTS, + NUM_PROJECTS + NUM_EMPTY_PROJECTS + NUM_BLOCKED_PROJECTS + ) + + for x in projects_range: + project = self.create_project( + x, + is_private=(x in [2, 4] or self.sd.boolean()), + blocked_code = BLOCKED_BY_STAFF if x in(blocked_projects_range) else None + ) # added memberships computable_project_roles = set() @@ -146,7 +164,7 @@ class Command(BaseCommand): Membership.objects.create(email=user.email, project=project, role=role, - is_owner=self.sd.boolean(), + is_admin=self.sd.boolean(), user=user) if role.computable: @@ -159,7 +177,7 @@ class Command(BaseCommand): Membership.objects.create(email=self.sd.email(), project=project, role=role, - is_owner=self.sd.boolean(), + is_admin=self.sd.boolean(), token=''.join(random.sample('abcdef0123456789', 10))) if role.computable: @@ -188,8 +206,8 @@ class Command(BaseCommand): project=project, order=i) - - if x < NUM_PROJECTS: + # If the project isn't empty + if x not in empty_projects_range: start_date = now() - datetime.timedelta(55) # create milestones @@ -281,6 +299,8 @@ class Command(BaseCommand): return self.sd.paragraphs(2, 4) if type == DATE_TYPE: return self.sd.future_date(min_distance=0, max_distance=365) + if type == URL_TYPE: + return self.sd.choice(URL_CHOICES) return None def create_bug(self, project): @@ -449,7 +469,7 @@ class Command(BaseCommand): return milestone - def create_project(self, counter, is_private=None): + def create_project(self, counter, is_private=None, blocked_code=None): if is_private is None: is_private=self.sd.boolean() @@ -467,7 +487,8 @@ class Command(BaseCommand): tags=self.sd.words(1, 10).split(" "), is_looking_for_people=counter in LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS, looking_for_people_note=self.sd.short_sentence(), - is_featured=counter in FEATURED_PROJECTS_POSITIONS) + is_featured=counter in FEATURED_PROJECTS_POSITIONS, + blocked_code=blocked_code) project.is_kanban_activated = True project.save() diff --git a/taiga/projects/migrations/0035_project_blocked_code.py b/taiga/projects/migrations/0035_project_blocked_code.py new file mode 100644 index 00000000..809e5cef --- /dev/null +++ b/taiga/projects/migrations/0035_project_blocked_code.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0034_project_looking_for_people_note'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='blocked_code', + field=models.CharField(choices=[('blocked-by-staff', 'This project was blocked by staff'), ('blocked-by-owner-leaving', 'This project was because the owner left')], null=True, default=None, max_length=255, blank=True, verbose_name='blocked code'), + ), + ] diff --git a/taiga/projects/migrations/0036_project_transfer_token.py b/taiga/projects/migrations/0036_project_transfer_token.py new file mode 100644 index 00000000..fbbdc28b --- /dev/null +++ b/taiga/projects/migrations/0036_project_transfer_token.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0035_project_blocked_code'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='transfer_token', + field=models.CharField(max_length=255, default=None, blank=True, null=True, verbose_name='project transfer token'), + ), + ] diff --git a/taiga/projects/migrations/0037_auto_20160208_1751.py b/taiga/projects/migrations/0037_auto_20160208_1751.py new file mode 100644 index 00000000..f0af4359 --- /dev/null +++ b/taiga/projects/migrations/0037_auto_20160208_1751.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0036_project_transfer_token'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='blocked_code', + field=models.CharField(max_length=255, blank=True, verbose_name='blocked code', choices=[('blocked-by-staff', 'This project was blocked by staff'), ('blocked-by-owner-leaving', 'This project was blocked because the owner left')], null=True, default=None), + ), + ] diff --git a/taiga/projects/migrations/0038_auto_20160215_1133.py b/taiga/projects/migrations/0038_auto_20160215_1133.py new file mode 100644 index 00000000..8c374d00 --- /dev/null +++ b/taiga/projects/migrations/0038_auto_20160215_1133.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-02-15 11:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0037_auto_20160208_1751'), + ] + + operations = [ + migrations.RenameField( + model_name='membership', + old_name='is_owner', + new_name='is_admin', + ), + ] diff --git a/taiga/projects/migrations/0039_auto_20160322_1157.py b/taiga/projects/migrations/0039_auto_20160322_1157.py new file mode 100644 index 00000000..51227138 --- /dev/null +++ b/taiga/projects/migrations/0039_auto_20160322_1157.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-03-22 11:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0038_auto_20160215_1133'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='blocked_code', + field=models.CharField(blank=True, choices=[('blocked-by-nonpayment', 'This project is blocked due to payment failure'), ('blocked-by-staff', 'This project is blocked by admin staff'), ('blocked-by-owner-leaving', 'This project is blocked because the owner left')], default=None, max_length=255, null=True, verbose_name='blocked code'), + ), + ] diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 2823f762..1728362b 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -22,6 +22,7 @@ from taiga.base import filters from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 from taiga.base.utils.db import get_object_or_none @@ -37,7 +38,8 @@ from . import permissions import datetime -class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): +class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, + BlockedByProjectMixin, ModelCrudViewSet): serializer_class = serializers.MilestoneSerializer permission_classes = (permissions.MilestonePermission,) filter_backends = (filters.CanViewMilestonesFilterBackend,) @@ -114,8 +116,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView 'estimated_finish': milestone.estimated_finish, 'total_points': total_points, 'completed_points': milestone.closed_points.values(), - 'total_userstories': milestone.get_cached_user_stories().count(), - 'completed_userstories': milestone.get_cached_user_stories().filter(is_closed=True).count(), + 'total_userstories': milestone.cached_user_stories.count(), + 'completed_userstories': milestone.cached_user_stories.filter(is_closed=True).count(), 'total_tasks': milestone.tasks.count(), 'completed_tasks': milestone.tasks.filter(status__is_closed=True).count(), 'iocaine_doses': milestone.tasks.filter(is_iocaine=True).count(), diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index 65459a6a..bf7bb469 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -22,6 +22,7 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.core.exceptions import ValidationError +from django.utils.functional import cached_property from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.dicts import dict_sum @@ -57,7 +58,6 @@ class Milestone(WatchedModelMixin, models.Model): verbose_name=_("order")) _importing = None _total_closed_points_by_date = None - _cached_user_stories = None class Meta: verbose_name = "milestone" @@ -87,13 +87,10 @@ class Milestone(WatchedModelMixin, models.Model): super().save(*args, **kwargs) - def get_cached_user_stories(self): - if self._cached_user_stories is None: - self._cached_user_stories = self.user_stories.\ - prefetch_related("role_points", "role_points__points").\ - annotate(num_tasks=Count("tasks")) - - return self._cached_user_stories + @cached_property + def cached_user_stories(self): + return (self.user_stories.prefetch_related("role_points", "role_points__points") + .annotate(num_tasks=Count("tasks"))) def _get_user_stories_points(self, user_stories): role_points = [us.role_points.all() for us in user_stories] @@ -104,63 +101,15 @@ class Milestone(WatchedModelMixin, models.Model): @property def total_points(self): return self._get_user_stories_points( - [us for us in self.get_cached_user_stories()] + [us for us in self.cached_user_stories] ) @property def closed_points(self): return self._get_user_stories_points( - [us for us in self.get_cached_user_stories() if us.is_closed] + [us for us in self.cached_user_stories if us.is_closed] ) - def _get_increment_points(self): - if hasattr(self, "_increments"): - return self._increments - - self._increments = { - "client_increment": {}, - "team_increment": {}, - "shared_increment": {}, - } - user_stories = UserStory.objects.none() - if self.estimated_start and self.estimated_finish: - user_stories = filter( - lambda x: x.created_date.date() >= self.estimated_start and x.created_date.date() < self.estimated_finish, - self.project.user_stories.all() - ) - self._increments['client_increment'] = self._get_user_stories_points( - [us for us in user_stories if us.client_requirement is True and us.team_requirement is False] - ) - self._increments['team_increment'] = self._get_user_stories_points( - [us for us in user_stories if us.client_requirement is False and us.team_requirement is True] - ) - self._increments['shared_increment'] = self._get_user_stories_points( - [us for us in user_stories if us.client_requirement is True and us.team_requirement is True] - ) - return self._increments - - - @property - def client_increment_points(self): - self._get_increment_points() - client_increment = self._get_increment_points()["client_increment"] - shared_increment = { - key: value/2 for key, value in self._get_increment_points()["shared_increment"].items() - } - return dict_sum(client_increment, shared_increment) - - @property - def team_increment_points(self): - team_increment = self._get_increment_points()["team_increment"] - shared_increment = { - key: value/2 for key, value in self._get_increment_points()["shared_increment"].items() - } - return dict_sum(team_increment, shared_increment) - - @property - def shared_increment_points(self): - return self._get_increment_points()["shared_increment"] - def total_closed_points_by_date(self, date): # Milestone instance will keep a cache of the total closed points by date if self._total_closed_points_by_date is None: @@ -168,7 +117,7 @@ class Milestone(WatchedModelMixin, models.Model): # We need to keep the milestone user stories indexed by id in a dict user_stories = {} - for us in self.get_cached_user_stories(): + for us in self.cached_user_stories: us._total_us_points = sum(self._get_user_stories_points([us]).values()) user_stories[us.id] = us diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py index 54462605..7404feb0 100644 --- a/taiga/projects/milestones/permissions.py +++ b/taiga/projects/milestones/permissions.py @@ -16,12 +16,12 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectOwner, AllowAny, + IsAuthenticated, IsProjectAdmin, AllowAny, IsSuperUser) class MilestonePermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_milestones') create_perms = HasProjectPerm('add_milestone') @@ -34,7 +34,7 @@ class MilestonePermission(TaigaResourcePermission): unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones') class MilestoneWatchersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_milestones') list_perms = HasProjectPerm('view_milestones') diff --git a/taiga/projects/mixins/ordering.py b/taiga/projects/mixins/ordering.py index 0a98c0a9..6917f1e3 100644 --- a/taiga/projects/mixins/ordering.py +++ b/taiga/projects/mixins/ordering.py @@ -54,6 +54,8 @@ class BulkUpdateOrderMixin: project = get_object_or_404(Project, id=project_id) self.check_permissions(request, 'bulk_update_order', project) - + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + self.__class__.bulk_update_order_action(project, request.user, bulk_data) return response.NoContent(data=None) diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 672ce9ee..59d16f9a 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -18,6 +18,7 @@ import itertools import uuid +from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.db.models import signals, Q @@ -27,6 +28,7 @@ from django.dispatch import receiver from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.utils.functional import cached_property from django_pgjson.fields import JsonField from djorm_pgarray.fields import TextArrayField @@ -69,7 +71,7 @@ class Membership(models.Model): related_name="memberships") role = models.ForeignKey("users.Role", null=False, blank=False, related_name="memberships") - is_owner = models.BooleanField(default=False, null=False, blank=False) + is_admin = models.BooleanField(default=False, null=False, blank=False) # Invitation metadata email = models.EmailField(max_length=255, default=None, null=True, blank=True, @@ -215,6 +217,13 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True, verbose_name=_("tags colors")) + transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None, + verbose_name=_("project transfer token")) + + blocked_code = models.CharField(null=True, blank=True, max_length=255, + choices=choices.BLOCKING_CODES + settings.EXTRA_BLOCKING_CODES, default=None, + verbose_name=_("blocked code")) + #Totals: totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, verbose_name=_("updated date time"), db_index=True) @@ -243,7 +252,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): total_activity_last_year = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("activity last year"), db_index=True) - _cached_user_stories = None _importing = None class Meta: @@ -278,6 +286,10 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): slug = "{}-{}".format(base_slug, i) self.slug = slug + if not self.is_backlog_activated: + self.total_milestones = None + self.total_story_points = None + if not self.videoconferences: self.videoconferences_extra_data = None @@ -329,13 +341,9 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): if save: self.save() - @property + @cached_property def cached_user_stories(self): - print(1111111, self._cached_user_stories) - if self._cached_user_stories is None: - self._cached_user_stories = list(self.user_stories.all()) - - return self._cached_user_stories + return list(self.user_stories.all()) def get_roles(self): return self.roles.all() diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 726e639b..fb3cada7 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -14,15 +14,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - from functools import partial from operator import is_not -from django.apps import apps -from django.contrib.contenttypes.models import ContentType +from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist -from django.db import models -from django.utils.translation import ugettext_lazy as _ from taiga.base import response from taiga.base.decorators import detail_route @@ -34,8 +30,6 @@ from taiga.projects.notifications.utils import (attach_watchers_to_queryset, attach_is_watcher_to_queryset, attach_total_watchers_to_queryset) -from taiga.users.models import User -from . import models from . serializers import WatcherSerializer @@ -45,9 +39,13 @@ class WatchedResourceMixin: Rest Framework resource mixin for resources susceptible to be notifiable about their changes. - NOTE: this mixin has hard dependency on HistoryMixin + NOTE: + - this mixin has hard dependency on HistoryMixin defined on history app and should be located always after it on inheritance definition. + + - the classes using this mixing must have a method: + def pre_conditions_on_save(self, obj) """ _not_notify = False @@ -64,6 +62,7 @@ class WatchedResourceMixin: def watch(self, request, pk=None): obj = self.get_object() self.check_permissions(request, "watch", obj) + self.pre_conditions_on_save(obj) services.add_watcher(obj, request.user) return response.Ok() @@ -71,6 +70,7 @@ class WatchedResourceMixin: def unwatch(self, request, pk=None): obj = self.get_object() self.check_permissions(request, "unwatch", obj) + self.pre_conditions_on_save(obj) services.remove_watcher(obj, request.user) return response.Ok() @@ -218,9 +218,9 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids)) removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids)) - User = apps.get_model("users", "User") - adding_users = User.objects.filter(id__in=adding_watcher_ids) - removing_users = User.objects.filter(id__in=removing_watcher_ids) + User = get_user_model() + adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids) + removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids) for user in adding_users: services.add_watcher(obj, user) @@ -233,13 +233,14 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): def to_native(self, obj): #if watchers wasn't attached via the get_queryset of the viewset we need to manually add it - if obj is not None and not hasattr(obj, "watchers"): - obj.watchers = [user.id for user in obj.get_watchers()] + if obj is not None: + if not hasattr(obj, "watchers"): + obj.watchers = [user.id for user in obj.get_watchers()] - request = self.context.get("request", None) - user = request.user if request else None - if user and user.is_authenticated(): - obj.is_watcher = user.id in obj.watchers + request = self.context.get("request", None) + user = request.user if request else None + if user and user.is_authenticated(): + obj.is_watcher = user.id in obj.watchers return super(WatchedResourceModelSerializer, self).to_native(obj) @@ -266,7 +267,7 @@ class WatchersViewSetMixin: try: self.object = resource.get_watchers().get(pk=pk) - except ObjectDoesNotExist: # or User.DoesNotExist + except ObjectDoesNotExist: # or User.DoesNotExist return response.NotFound() serializer = self.get_serializer(self.object) diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py index ef56ace7..9c36fe75 100644 --- a/taiga/projects/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -16,7 +16,8 @@ # along with this program. If not, see . from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey + from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -32,7 +33,7 @@ class NotifyPolicy(models.Model): project user notifications preference. """ project = models.ForeignKey("projects.Project", related_name="notify_policies") - user = models.ForeignKey("users.User", related_name="notify_policies") + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="notify_policies") notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES) created_at = models.DateTimeField(default=timezone.now) @@ -56,7 +57,7 @@ class HistoryChangeNotification(models.Model): or updated when an object requires notifications. """ key = models.CharField(max_length=255, unique=False, editable=False) - owner = models.ForeignKey("users.User", null=False, blank=False, + owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, verbose_name=_("owner"), related_name="+") created_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, verbose_name=_("created date time")) @@ -65,7 +66,7 @@ class HistoryChangeNotification(models.Model): history_entries = models.ManyToManyField("history.HistoryEntry", verbose_name=_("history entries"), related_name="+") - notify_users = models.ManyToManyField("users.User", + notify_users = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("notify users"), related_name="+") project = models.ForeignKey("projects.Project", null=False, blank=False, @@ -80,7 +81,7 @@ class HistoryChangeNotification(models.Model): class Watched(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, related_name="watched", verbose_name=_("user")) created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, diff --git a/taiga/projects/notifications/serializers.py b/taiga/projects/notifications/serializers.py index 8eab93a0..ed93dce7 100644 --- a/taiga/projects/notifications/serializers.py +++ b/taiga/projects/notifications/serializers.py @@ -15,13 +15,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import json - from taiga.base.api import serializers -from taiga.users.models import User +from taiga.users.models import get_user_model_safe from . import models -from . import choices class NotifyPolicySerializer(serializers.ModelSerializer): @@ -39,5 +36,5 @@ class WatcherSerializer(serializers.ModelSerializer): full_name = serializers.CharField(source='get_full_name', required=False) class Meta: - model = User + model = get_user_model_safe() fields = ('id', 'username', 'full_name') diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 1763ed7b..0a3cb8e7 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -20,7 +20,6 @@ import datetime from functools import partial from django.apps import apps -from django.db.transaction import atomic from django.db import IntegrityError, transaction from django.db.models import Q from django.contrib.contenttypes.models import ContentType @@ -37,7 +36,6 @@ from taiga.projects.history.services import (make_key_from_model_object, get_last_snapshot_for_key, get_model_from_key) from taiga.permissions.service import user_has_perm -from taiga.users.models import User from .models import HistoryChangeNotification, Watched @@ -102,13 +100,13 @@ def analize_object_for_watchers(obj:object, comment:str, user:object): if not hasattr(obj, "add_watcher"): return - from taiga import mdrender as mdr texts = (getattr(obj, "description", ""), getattr(obj, "content", ""), comment,) - _, data = mdr.render_and_extract(obj.get_project(), "\n".join(texts)) + from taiga.mdrender.service import render_and_extract + _, data = render_and_extract(obj.get_project(), "\n".join(texts)) if data["mentions"]: for user in data["mentions"]: @@ -214,7 +212,7 @@ def send_notifications(obj, *, history): return None key = make_key_from_model_object(obj) - owner = User.objects.get(pk=history.user["pk"]) + owner = get_user_model().objects.get(pk=history.user["pk"]) notification, created = (HistoryChangeNotification.objects.select_for_update() .get_or_create(key=key, owner=owner, diff --git a/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja index c07b9a92..47ce994d 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/updates-body-html.jinja" %} {% block head %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %}

Issue updated

Hello {{ user }},
{{ changer }} has updated an issue on {{ project }}

Issue #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja index adda2149..0f5fd94e 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja @@ -1,6 +1,6 @@ {% extends "emails/updates-body-text.jinja" %} {% block head %} -{% trans user=user.get_full_name(), changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %} Issue updated Hello {{ user }}, {{ changer }} has updated an issue on {{ project }} See issue #{{ ref }} {{ subject }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja index 707e2703..5cee74b7 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Updated the issue #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja index 8147f59c..5560899a 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %}

New issue created

Hello {{ user }},
{{ changer }} has created a new issue on {{ project }}

Issue #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja index 5b7ff367..3a69643c 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("issue", project.slug, snapshot.ref) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("issue", project.slug, snapshot.ref) %} New issue created Hello {{ user }}, {{ changer }} has created a new issue on {{ project }} See issue #{{ ref }} {{ subject }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja index 7e4cf6bd..cde770ce 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Created the issue #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja index 7b184627..1b20a020 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}

Issue deleted

Hello {{ user }},
{{ changer }} has deleted an issue on {{ project }}

Issue #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja index 7c39fc31..40c386f9 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %} Issue deleted Hello {{ user }}, {{ changer }} has deleted an issue on {{ project }} Issue #{{ ref }} {{ subject }} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja index bf297fa4..c5773908 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Deleted the issue #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja index f756b38c..3e350766 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/updates-body-html.jinja" %} {% block head %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}

Sprint updated

Hello {{ user }},
{{ changer }} has updated an sprint on {{ project }}

Sprint {{ name }}

diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja index f98e35cb..dbb65b00 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja @@ -1,6 +1,6 @@ {% extends "emails/updates-body-text.jinja" %} {% block head %} -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("task", project.slug, snapshot.slug) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("task", project.slug, snapshot.slug) %} Sprint updated Hello {{ user }}, {{ changer }} has updated a sprint on {{ project }} See sprint {{ name }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja index 400bf944..1891a893 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, milestone=snapshot.name|safe %} +{% trans project=project.name, milestone=snapshot.name %} [{{ project }}] Updated the sprint "{{ milestone }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja index 8390b282..39de7a4a 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %}

New sprint created

Hello {{ user }},
{{ changer }} has created a new sprint on {{ project }}

Sprint {{ name }}

diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja index 0433cf7f..8cbd531b 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name, url=resolve_front_url("taskboard", project.slug, snapshot.slug) %} New sprint created Hello {{ user }}, {{ changer }} has created a new sprint on {{ project }} See sprint {{ name }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja index 10656b83..700faac0 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, milestone=snapshot.name|safe %} +{% trans project=project.name, milestone=snapshot.name %} [{{ project }}] Created the sprint "{{ milestone }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja index fcd2cb2d..4604ba41 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, name=snapshot.name|safe %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, name=snapshot.name %}

Sprint deleted

Hello {{ user }},
{{ changer }} has deleted an sprint on {{ project }}

Sprint {{ name }}

diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja index 2bb8bbe1..4c19f2cb 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, name=snapshot.name|safe %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, name=snapshot.name %} Sprint deleted Hello {{ user }}, {{ changer }} has deleted an sprint on {{ project }} Sprint {{ name }} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja index 11404786..242f1326 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, milestone=snapshot.name|safe %} +{% trans project=project.name, milestone=snapshot.name %} [{{ project }}] Deleted the Sprint "{{ milestone }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja index 024a41f2..83093f1f 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/updates-body-html.jinja" %} {% block head %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %}

Task updated

Hello {{ user }},
{{ changer }} has updated a task on {{ project }}

Task #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja index f34a7d96..45c3015b 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja @@ -1,6 +1,6 @@ {% extends "emails/updates-body-text.jinja" %} {% block head %} -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %} Task updated Hello {{ user }}, {{ changer }} has updated a task on {{ project }} See task #{{ ref }} {{ subject }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja index 1e4d7c16..6275e839 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Updated the task #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja index bd0e796e..496c77af 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %}

New task created

Hello {{ user }},
{{ changer }} has created a new task on {{ project }}

Task #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja index 34deae44..befa385c 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("task", project.slug, snapshot.ref) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("task", project.slug, snapshot.ref) %} New task created Hello {{ user }}, {{ changer }} has created a new task on {{ project }} See task #{{ ref }} {{ subject }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja index d107e855..27dabde4 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Created the task #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja index d10551cd..0eb1af1e 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}

Task deleted

Hello {{ user }},
{{ changer }} has deleted a task on {{ project }}

Task #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja index d2c11cdf..9234770b 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %} Task deleted Hello {{ user }}, {{ changer }} has deleted a task on {{ project }} Task #{{ ref }} {{ subject }} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja index e80e164f..8522f00c 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Deleted the task #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja index 8cecace4..0ca026c2 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/updates-body-html.jinja" %} {% block head %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}

User Story updated

Hello {{ user }},
{{ changer }} has updated a user story on {{ project }}

User Story #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja index 68719440..d9fc6d71 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja @@ -1,6 +1,6 @@ {% extends "emails/updates-body-text.jinja" %} {% block head %} -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe,ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name,ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %} User story updated Hello {{ user }}, {{ changer }} has updated a user story on {{ project }} See user story #{{ ref }} {{ subject }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja index e116e403..3ed70685 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Updated the US #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja index dc35ed3d..a1e4a8a1 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %}

New user story created

Hello {{ user }},
{{ changer }} has created a new user story on {{ project }}

User Story #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja index 8bcd625c..842fc0d2 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe, url=resolve_front_url("userstory", project.slug, snapshot.ref) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("userstory", project.slug, snapshot.ref) %} New user story created Hello {{ user }}, {{ changer }} has created a new user story on {{ project }} See user story #{{ ref }} {{ subject }} at {{ url }} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja index 124e3060..1e166e57 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Created the US #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja index 8decb3ef..1877b3b7 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %}

User Story deleted

Hello {{ user }},
{{ changer }} has deleted a user story on {{ project }}

User Story #{{ ref }} {{ subject }}

diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja index 0158a1b8..b03e4879 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %} User Story deleted Hello {{ user }}, {{ changer }} has deleted a user story on {{ project }} User Story #{{ ref }} {{ subject }} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja index f7843217..3a48058e 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} [{{ project }}] Deleted the US #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja index 550c7ebe..f056254c 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/updates-body-html.jinja" %} {% block head %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}

Wiki Page updated

Hello {{ user }},
{{ changer }} has updated a wiki page on {{ project }}

Wiki page {{ page }}

diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja index 31f5884a..c20da951 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja @@ -1,6 +1,6 @@ {% extends "emails/updates-body-text.jinja" %} {% block head %} -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %} Wiki Page updated Hello {{ user }}, {{ changer }} has updated a wiki page on {{ project }} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja index 251a5adb..73912cb6 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, page=snapshot.slug %} +{% trans project=project.name, page=snapshot.slug %} [{{ project }}] Updated the Wiki Page "{{ page }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja index 7627ef2b..28b1a0b5 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %}

New wiki page created

Hello {{ user }},
{{ changer }} has created a new wiki page on {{ project }}

Wiki page {{ page }}

diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja index 29d5e375..1a1e45af 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug, url=resolve_front_url("wiki", project.slug, snapshot.slug) %} New wiki page created Hello {{ user }}, {{ changer }} has created a new wiki page on {{ project }} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja index ac5a6ddf..6b7d1adf 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, page=snapshot.slug %} +{% trans project=project.name, page=snapshot.slug %} [{{ project }}] Created the Wiki Page "{{ page }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja index 212441cf..291c1cb6 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja @@ -1,7 +1,7 @@ {% extends "emails/base-body-html.jinja" %} {% block body %} - {% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug %}

Wiki page deleted

Hello {{ user }},
{{ changer }} has deleted a wiki page on {{ project }}

Wiki page {{ page }}

diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja index a53b87a5..64745aed 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja @@ -1,4 +1,4 @@ -{% trans user=user.get_full_name()|safe, changer=changer.get_full_name()|safe, project=project.name|safe, page=snapshot.slug %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, page=snapshot.slug %} Wiki page deleted Hello {{ user }}, {{ changer }} has deleted a wiki page on {{ project }} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja index d73de78f..a807f6f3 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name|safe, page=snapshot.slug %} +{% trans project=project.name, page=snapshot.slug %} [{{ project }}] Deleted the Wiki Page "{{ page }}" {% endtrans %} diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index c1e017a7..de65cd69 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api.permissions import TaigaResourcePermission from taiga.base.api.permissions import HasProjectPerm from taiga.base.api.permissions import IsAuthenticated -from taiga.base.api.permissions import IsProjectOwner +from taiga.base.api.permissions import IsProjectAdmin from taiga.base.api.permissions import AllowAny from taiga.base.api.permissions import IsSuperUser from taiga.base.api.permissions import PermissionComponent @@ -37,30 +37,38 @@ class CanLeaveProject(PermissionComponent): try: if not services.can_user_leave_project(request.user, obj): - raise exc.PermissionDenied(_("You can't leave the project if there are no " - "more owners")) + raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are no more admins")) return True except Membership.DoesNotExist: return False +class IsMainOwner(PermissionComponent): + def check_permissions(self, request, view, obj=None): + if not obj or not request.user.is_authenticated(): + return False + + if obj.owner is None: + return False + + return obj.owner == request.user class ProjectPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') by_slug_perms = HasProjectPerm('view_project') create_perms = IsAuthenticated() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() - modules_perms = IsProjectOwner() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() + modules_perms = IsProjectAdmin() list_perms = AllowAny() - change_logo_perms = IsProjectOwner() - remove_logo_perms = IsProjectOwner() + change_logo_perms = IsProjectAdmin() + remove_logo_perms = IsProjectAdmin() stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project') issues_stats_perms = HasProjectPerm('view_project') - regenerate_userstories_csv_uuid_perms = IsProjectOwner() - regenerate_issues_csv_uuid_perms = IsProjectOwner() - regenerate_tasks_csv_uuid_perms = IsProjectOwner() + regenerate_userstories_csv_uuid_perms = IsProjectAdmin() + regenerate_issues_csv_uuid_perms = IsProjectAdmin() + regenerate_tasks_csv_uuid_perms = IsProjectAdmin() tags_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project') like_perms = IsAuthenticated() & HasProjectPerm('view_project') @@ -69,17 +77,22 @@ class ProjectPermission(TaigaResourcePermission): unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project') create_template_perms = IsSuperUser() leave_perms = CanLeaveProject() + transfer_validate_token_perms = IsAuthenticated() & HasProjectPerm('view_project') + transfer_request_perms = IsProjectAdmin() + transfer_start_perms = IsMainOwner() + transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project') + transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project') class ProjectFansPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_project') list_perms = HasProjectPerm('view_project') class ProjectWatchersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_project') list_perms = HasProjectPerm('view_project') @@ -87,89 +100,89 @@ class ProjectWatchersPermission(TaigaResourcePermission): class MembershipPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_create_perms = IsProjectOwner() - resend_invitation_perms = IsProjectOwner() + bulk_create_perms = IsProjectAdmin() + resend_invitation_perms = IsProjectAdmin() # User Stories class PointsPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() class UserStoryStatusPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() # Tasks class TaskStatusPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() # Issues class SeverityPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() class PriorityPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() class IssueStatusPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() class IssueTypePermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - bulk_update_order_perms = IsProjectOwner() + bulk_update_order_perms = IsProjectAdmin() # Project Templates diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py index f8b196d9..bf3b072c 100644 --- a/taiga/projects/references/models.py +++ b/taiga/projects/references/models.py @@ -18,7 +18,7 @@ from django.db import models from django.utils import timezone from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.generic import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task diff --git a/taiga/projects/references/permissions.py b/taiga/projects/references/permissions.py index a62d5a09..baa91d8c 100644 --- a/taiga/projects/references/permissions.py +++ b/taiga/projects/references/permissions.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectOwner, AllowAny) + IsProjectAdmin, AllowAny) class ResolverPermission(TaigaResourcePermission): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 04bebd11..829eec89 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -33,7 +33,7 @@ from taiga.users.serializers import ProjectRoleSerializer from taiga.users.validators import RoleExistsValidator from taiga.permissions.service import get_user_project_permissions -from taiga.permissions.service import is_project_owner +from taiga.permissions.service import is_project_admin, is_project_owner from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin from . import models @@ -76,7 +76,6 @@ class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin): class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer): - class Meta: model = models.TaskStatus i18n_fields = ("name",) @@ -130,6 +129,7 @@ class MembershipSerializer(serializers.ModelSerializer): project_name = serializers.SerializerMethodField("get_project_name") project_slug = serializers.SerializerMethodField("get_project_slug") invited_by = UserBasicInfoSerializer(read_only=True) + is_owner = serializers.SerializerMethodField("get_is_owner") class Meta: model = models.Membership @@ -147,6 +147,10 @@ class MembershipSerializer(serializers.ModelSerializer): def get_project_slug(self, obj): return obj.project.slug if obj and obj.project else "" + def get_is_owner(self, obj): + return (obj and obj.user_id and obj.project_id and obj.project.owner_id and + obj.user_id == obj.project.owner_id) + def validate_email(self, attrs, source): project = attrs.get("project", None) if project is None: @@ -181,15 +185,17 @@ class MembershipSerializer(serializers.ModelSerializer): return attrs - def validate_is_owner(self, attrs, source): - is_owner = attrs[source] + def validate_is_admin(self, attrs, source): project = attrs.get("project", None) if project is None: project = self.object.project - if (self.object and - not services.project_has_valid_owners(project, exclude_user=self.object.user)): - raise serializers.ValidationError(_("At least one of the user must be an active admin")) + if (self.object and self.object.user): + if self.object.user.id == project.owner_id and attrs[source] != True: + raise serializers.ValidationError(_("The project owner must be admin.")) + + if not services.project_has_valid_admins(project, exclude_user=self.object.user): + raise serializers.ValidationError(_("At least one user must be an active admin for this project.")) return attrs @@ -242,7 +248,10 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ anon_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False) my_permissions = serializers.SerializerMethodField("get_my_permissions") + + owner = UserBasicInfoSerializer(read_only=True) i_am_owner = serializers.SerializerMethodField("get_i_am_owner") + i_am_admin = serializers.SerializerMethodField("get_i_am_admin") i_am_member = serializers.SerializerMethodField("get_i_am_member") tags = TagsField(default=[], required=False) @@ -257,9 +266,10 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ class Meta: model = models.Project - read_only_fields = ("created_date", "modified_date", "owner", "slug") + read_only_fields = ("created_date", "modified_date", "slug", "blocked_code") exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref", - "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid") + "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid", + "transfer_token") def get_my_permissions(self, obj): if "request" in self.context: @@ -271,6 +281,11 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ return is_project_owner(self.context["request"].user, obj) return False + def get_i_am_admin(self, obj): + if "request" in self.context: + return is_project_admin(self.context["request"].user, obj) + return False + def get_i_am_member(self, obj): if "request" in self.context: user = self.context["request"].user @@ -328,6 +343,7 @@ class ProjectDetailSerializer(ProjectSerializer): roles = ProjectRoleSerializer(source="roles", many=True, read_only=True) members = serializers.SerializerMethodField(method_name="get_members") + total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships") def get_members(self, obj): qs = obj.memberships.filter(user__isnull=False) @@ -337,13 +353,27 @@ class ProjectDetailSerializer(ProjectSerializer): serializer = ProjectMemberSerializer(qs, many=True) return serializer.data + def get_total_memberships(self, obj): + return services.get_total_project_memberships(obj) + class ProjectDetailAdminSerializer(ProjectDetailSerializer): + is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info") + max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships") + class Meta: model = models.Project - read_only_fields = ("created_date", "modified_date", "owner", "slug") + read_only_fields = ("created_date", "modified_date", "slug", "blocked_code") exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref") + def get_is_private_extra_info(self, obj): + return services.check_if_project_privacity_can_be_changed(obj) + + def get_max_memberships(self, obj): + return services.get_max_memberships_for_project(obj) + + + ###################################################### ## Liked diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index d4940c37..6227cc54 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -37,7 +37,9 @@ from .logo import get_logo_big_thumbnail_url from .members import create_members_in_bulk from .members import get_members_from_bulk -from .members import remove_user_from_project, project_has_valid_owners, can_user_leave_project +from .members import remove_user_from_project, project_has_valid_admins, can_user_leave_project +from .members import get_max_memberships_for_project, get_total_project_memberships +from .members import check_if_project_privacity_can_be_changed from .modules_config import get_modules_config @@ -46,3 +48,7 @@ from .stats import get_stats_for_project from .stats import get_member_stats_for_project from .tags_colors import update_project_tags_colors_handler +from .modules_config import get_modules_config + +from .transfer import request_project_transfer, start_project_transfer +from .transfer import accept_project_transfer, reject_project_transfer diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py index f4efc5e9..f408d430 100644 --- a/taiga/projects/services/members.py +++ b/taiga/projects/services/members.py @@ -36,23 +36,87 @@ def remove_user_from_project(user, project): models.Membership.objects.get(project=project, user=user).delete() -def project_has_valid_owners(project, exclude_user=None): +def project_has_valid_admins(project, exclude_user=None): """ Checks if the project has any owner membership with a user different than the specified """ - owner_memberships = project.memberships.filter(is_owner=True, user__is_active=True) + admin_memberships = project.memberships.filter(is_admin=True, user__is_active=True) if exclude_user: - owner_memberships = owner_memberships.exclude(user=exclude_user) + admin_memberships = admin_memberships.exclude(user=exclude_user) - return owner_memberships.count() > 0 + return admin_memberships.count() > 0 def can_user_leave_project(user, project): membership = project.memberships.get(user=user) - if not membership.is_owner: + if not membership.is_admin: return True - if not project_has_valid_owners(project, exclude_user=user): + #The user can't leave if is the real owner of the project + if project.owner == user: + return False + + if not project_has_valid_admins(project, exclude_user=user): return False return True + + +def get_max_memberships_for_project(project): + """Return tha maximun of membersh for a concrete project. + + :param project: A project object. + + :return: a number or null. + """ + if project.is_private: + return project.owner.max_memberships_private_projects + return project.owner.max_memberships_public_projects + + +def get_total_project_memberships(project): + """Return tha total of memberships of a project (members and unaccepted invitations). + + :param project: A project object. + + :return: a number. + """ + return project.memberships.count() + + +ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships' +ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships' +ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects' +ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects' + +def check_if_project_privacity_can_be_changed(project): + """Return if the project privacity can be changed from private to public or viceversa. + + :param project: A project object. + + :return: True if it can be changed or False if can't. + """ + if project.is_private: + current_memberships = project.memberships.count() + max_memberships = project.owner.max_memberships_public_projects + error_members_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS + + current_projects = project.owner.owned_projects.filter(is_private=False).count() + max_projects = project.owner.max_public_projects + error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS + else: + current_memberships = project.memberships.count() + max_memberships = project.owner.max_memberships_private_projects + error_members_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS + + current_projects = project.owner.owned_projects.filter(is_private=True).count() + max_projects = project.owner.max_private_projects + error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS + + if max_memberships is not None and current_memberships > max_memberships: + return {'can_be_updated': False, 'reason': error_members_exceeded} + + if max_projects is not None and current_projects >= max_projects: + return {'can_be_updated': False, 'reason': error_project_exceeded} + + return {'can_be_updated': True, 'reason': None} diff --git a/taiga/projects/services/modules_config.py b/taiga/projects/services/modules_config.py index 46cf6b9a..b850be56 100644 --- a/taiga/projects/services/modules_config.py +++ b/taiga/projects/services/modules_config.py @@ -24,7 +24,7 @@ from django.conf import settings def get_modules_config(project): modules_config, created = models.ProjectModulesConfig.objects.get_or_create(project=project) - if created: + if created or modules_config.config == None: modules_config.config = {} for key, configurator_function_name in settings.PROJECT_MODULES_CONFIGURATORS.items(): diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py index 6e12ff06..0972dc6b 100644 --- a/taiga/projects/services/stats.py +++ b/taiga/projects/services/stats.py @@ -22,8 +22,6 @@ import datetime import copy import collections -from taiga.projects.history.models import HistoryEntry -from taiga.projects.userstories.models import RolePoints def _count_status_object(status_obj, counting_storage): if status_obj.id in counting_storage: @@ -228,6 +226,7 @@ def _get_milestones_stats_for_backlog(project, milestones): def get_stats_for_project(project): # Let's fetch all the estimations related to a project with all the necesary # related data + RolePoints = apps.get_model('userstories', 'RolePoints') role_points = RolePoints.objects.filter( user_story__project = project, ).prefetch_related( @@ -378,6 +377,7 @@ def _get_wiki_changes_per_member_stats(project): # Wiki changes wiki_changes = {} wiki_page_keys = ["wiki.wikipage:%s"%id for id in project.wiki_pages.values_list("id", flat=True)] + HistoryEntry = apps.get_model('history', 'HistoryEntry') history_entries = HistoryEntry.objects.filter(key__in=wiki_page_keys).values('user') for entry in history_entries: editions = wiki_changes.get(entry["user"]["pk"], 0) diff --git a/taiga/projects/services/transfer.py b/taiga/projects/services/transfer.py new file mode 100644 index 00000000..bef18577 --- /dev/null +++ b/taiga/projects/services/transfer.py @@ -0,0 +1,114 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.core import signing +from django.utils.translation import ugettext as _ + +import datetime + +from taiga.base.mails import mail_builder +from taiga.base import exceptions as exc + + +def request_project_transfer(project, user): + template = mail_builder.transfer_request + email = template(project.owner, {"project": project, "requester": user}) + email.send() + + +def start_project_transfer(project, user, reason): + """Generates the transfer token for a project transfer and notify to the destination user + + :param project: Project trying to transfer + :param user: Destination user + :param reason: Reason to transfer the project + """ + + signer = signing.TimestampSigner() + token = signer.sign(user.id) + project.transfer_token = token + project.save() + + template = mail_builder.transfer_start + context = { + "project": project, + "receiver": user, + "token": token, + "reason": reason + } + email = template(user, context) + email.send() + + +def validate_project_transfer_token(token, project, user): + signer = signing.TimestampSigner() + + if project.transfer_token != token: + raise exc.WrongArguments(_("Token is invalid")) + + try: + value = signer.unsign(token, max_age=datetime.timedelta(days=7)) + except signing.SignatureExpired: + raise exc.WrongArguments(_("Token has expired")) + except signing.BadSignature: + raise exc.WrongArguments(_("Token is invalid")) + + if str(value) != str(user.id): + raise exc.WrongArguments(_("Token is invalid")) + + +def reject_project_transfer(project, user, token, reason): + validate_project_transfer_token(token, project, user) + + project.transfer_token = None + project.save() + + template = mail_builder.transfer_reject + context = { + "project": project, + "rejecter": user, + "reason": reason + } + email = template(project.owner, context) + email.send() + + +def accept_project_transfer(project, user, token, reason): + validate_project_transfer_token(token, project, user) + + # Set new owner as project admin + membership = project.memberships.get(user=user) + if not membership.is_admin: + membership.is_admin = True + membership.save() + + # Change the owner of the project + old_owner = project.owner + project.transfer_token = None + project.owner = user + project.save() + + # Send mail + template = mail_builder.transfer_accept + context = { + "project": project, + "old_owner": old_owner, + "new_owner": user, + "reason": reason + } + email = template(old_owner, context) + email.send() diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py index afae4b5a..51ff6485 100644 --- a/taiga/projects/signals.py +++ b/taiga/projects/signals.py @@ -85,7 +85,7 @@ def project_post_save(sender, instance, created, **kwargs): if owner_role: Membership = apps.get_model("projects", "Membership") Membership.objects.create(user=instance.owner, project=instance, role=owner_role, - is_owner=True, email=instance.owner.email) + is_admin=True, email=instance.owner.email) ## US statuses diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 8d4cf373..6bb2a1a3 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -22,6 +22,7 @@ from taiga.base import filters, response from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.projects.models import Project, TaskStatus from django.http import HttpResponse @@ -38,7 +39,7 @@ from . import services class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - ModelCrudViewSet): + BlockedByProjectMixin, ModelCrudViewSet): queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter) @@ -95,7 +96,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa "assigned_to", "status", "project") - + return self.attach_watchers_attrs_to_queryset(qs) def pre_save(self, obj): @@ -147,6 +148,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa data = serializer.data project = Project.objects.get(id=data["project_id"]) self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + tasks = services.create_tasks_in_bulk( data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], status_id=data.get("status_id") or project.default_task_status_id, @@ -166,6 +170,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa project = get_object_or_404(Project, pk=data["project_id"]) self.check_permissions(request, "bulk_update_order", project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + services.update_tasks_order_in_bulk(data["bulk_tasks"], project=project, field=order_field) diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 3284ef1d..ad0209a0 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -19,11 +19,10 @@ from django.apps import AppConfig from django.apps import apps from django.db.models import signals -from taiga.projects import signals as generic_handlers -from taiga.projects.custom_attributes import signals as custom_attributes_handlers -from . import signals as handlers def connect_tasks_signals(): + from taiga.projects import signals as generic_handlers + from . import signals as handlers # Finished date signals.pre_save.connect(handlers.set_finished_date_when_edit_task, sender=apps.get_model("tasks", "Task"), @@ -40,6 +39,7 @@ def connect_tasks_signals(): dispatch_uid="update_project_tags_when_delete_tagglabe_item_task") def connect_tasks_close_or_open_us_and_milestone_signals(): + from . import signals as handlers # Cached prev object version signals.pre_save.connect(handlers.cached_prev_task, sender=apps.get_model("tasks", "Task"), @@ -53,6 +53,7 @@ def connect_tasks_close_or_open_us_and_milestone_signals(): dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") def connect_tasks_custom_attributes_signals(): + from taiga.projects.custom_attributes import signals as custom_attributes_handlers signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task, sender=apps.get_model("tasks", "Task"), dispatch_uid="create_custom_attribute_value_when_create_task") diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 0340f8d6..a1e945f1 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -62,7 +62,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None, related_name="tasks_assigned_to_me", verbose_name=_("assigned to")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") is_iocaine = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is iocaine")) external_reference = TextArrayField(default=None, verbose_name=_("external reference")) diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 4cb3f691..9e189f10 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -16,12 +16,12 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectOwner, AllowAny, + IsAuthenticated, IsProjectAdmin, AllowAny, IsSuperUser) class TaskPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_tasks') create_perms = HasProjectPerm('add_task') @@ -39,14 +39,14 @@ class TaskPermission(TaigaResourcePermission): class TaskVotersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_tasks') list_perms = HasProjectPerm('view_tasks') class TaskWatchersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_tasks') list_perms = HasProjectPerm('view_tasks') diff --git a/taiga/projects/templates/emails/membership_invitation-subject.jinja b/taiga/projects/templates/emails/membership_invitation-subject.jinja index 0b5206ef..8e620317 100644 --- a/taiga/projects/templates/emails/membership_invitation-subject.jinja +++ b/taiga/projects/templates/emails/membership_invitation-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=membership.project|safe %} +{% trans project=membership.project %} [Taiga] Invitation to join to the project '{{ project }}' {% endtrans %} diff --git a/taiga/projects/templates/emails/membership_notification-subject.jinja b/taiga/projects/templates/emails/membership_notification-subject.jinja index 57d60ac6..c6bdd588 100644 --- a/taiga/projects/templates/emails/membership_notification-subject.jinja +++ b/taiga/projects/templates/emails/membership_notification-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=membership.project|safe %} +{% trans project=membership.project %} [Taiga] Added to the project '{{ project }}' {% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_accept-body-html.jinja b/taiga/projects/templates/emails/transfer_accept-body-html.jinja new file mode 100644 index 00000000..6c8f409e --- /dev/null +++ b/taiga/projects/templates/emails/transfer_accept-body-html.jinja @@ -0,0 +1,17 @@ +{% extends "emails/base-body-html.jinja" %} + +{% block body %} + {% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %} +

Hi {{old_owner_name}},

+

{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".

+ {% endtrans %} + + {% if reason %} + {% trans new_owner_name=new_owner.get_full_name()%}

{{ new_owner_name }} says:

{% endtrans %} +

{{reason}}

+ {% endif %} + + {% trans %} +

From now on, your new status for this project will be "admin".

+ {% endtrans %} +{% endblock %} diff --git a/taiga/projects/templates/emails/transfer_accept-body-text.jinja b/taiga/projects/templates/emails/transfer_accept-body-text.jinja new file mode 100644 index 00000000..0fda1124 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_accept-body-text.jinja @@ -0,0 +1,18 @@ +{% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %} +Hi {{old_owner_name}}, +{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}". +{% endtrans %} + +{% if reason %} +{% trans new_owner_name=new_owner.get_full_name()%}{{ new_owner_name }} says:{% endtrans %} +{{reason}} +{% endif %} + +{% trans %} +From now on, your new status for this project will be "admin". +{% endtrans %} + +--- +{% trans %} +The Taiga Team +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_accept-subject.jinja b/taiga/projects/templates/emails/transfer_accept-subject.jinja new file mode 100644 index 00000000..6b7c84d5 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_accept-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name %} +[{{project}}] Project ownership transfer offer accepted! +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_reject-body-html.jinja b/taiga/projects/templates/emails/transfer_reject-body-html.jinja new file mode 100644 index 00000000..982915f7 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_reject-body-html.jinja @@ -0,0 +1,24 @@ +{% extends "emails/base-body-html.jinja" %} + +{% block body %} + {% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %} +

Hi {{ owner_name }},

+

{{ rejecter_name }} has declined your offer and will not become the new project owner for "{{project_name}}".

+ {% endtrans %} + + {% if reason %} + {% trans rejecter_name=rejecter.get_full_name()%} +

{{ rejecter_name }} says:

+ {% endtrans %} +

{{ reason }}

+ {% endif %} + + {% trans %} +

If you want, you can still try to transfer the project ownership to a different person.

+ {% endtrans %} + +
+ {% trans %}Request transfer to a different person{% endtrans %} + +{% endblock %} diff --git a/taiga/projects/templates/emails/transfer_reject-body-text.jinja b/taiga/projects/templates/emails/transfer_reject-body-text.jinja new file mode 100644 index 00000000..972cce54 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_reject-body-text.jinja @@ -0,0 +1,21 @@ +{% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %} +Hi {{owner_name}}, +{{ rejecter_name}} has declined your offer and will not become the new project owner for "{{project_name}}". +{% endtrans %} + +{% if reason %} +{% trans rejecter_name=rejecter.get_full_name()%}{{ rejecter_name }} says:{% endtrans %} +{{ reason }} +{% endif %} + +{% trans %} +If you want, you can still try to transfer the project ownership to a different person. +{% endtrans %} + +{% trans %}Request transfer to a different person:{% endtrans %} +{{ resolve_front_url("project-admin", project.slug) }} + +--- +{% trans %} +The Taiga Team +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_reject-subject.jinja b/taiga/projects/templates/emails/transfer_reject-subject.jinja new file mode 100644 index 00000000..e6eaa127 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_reject-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name %} +[{{project}}] Project ownership transfer declined +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_request-body-html.jinja b/taiga/projects/templates/emails/transfer_request-body-html.jinja new file mode 100644 index 00000000..3351c382 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_request-body-html.jinja @@ -0,0 +1,15 @@ +{% extends "emails/base-body-html.jinja" %} + +{% block body %} + {% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %} +

Hi {{owner_name}},

+

{{ requester_name }} has requested to become the project owner for "{{project_name}}".

+ {% endtrans %} + + {% trans %} +

Please, click on "Continue" if you would like to start the project transfer from the administration panel.

+ {% endtrans %} + + {% trans %}Continue{% endtrans %} +{% endblock %} diff --git a/taiga/projects/templates/emails/transfer_request-body-text.jinja b/taiga/projects/templates/emails/transfer_request-body-text.jinja new file mode 100644 index 00000000..9b156a4d --- /dev/null +++ b/taiga/projects/templates/emails/transfer_request-body-text.jinja @@ -0,0 +1,15 @@ +{% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %} +Hi {{owner_name}}, +{{ requester_name }} has requested to become the project owner for "{{project_name}}". +{% endtrans %} + +{% trans %} +Please, go to your project settings if you would like to start the project transfer from the administration panel. +{% endtrans %} + +{{ _("Go to your project settings:") }} {{ resolve_front_url("project-admin", project.slug) }} + +--- +{% trans %} +The Taiga Team +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_request-subject.jinja b/taiga/projects/templates/emails/transfer_request-subject.jinja new file mode 100644 index 00000000..1f6ff81c --- /dev/null +++ b/taiga/projects/templates/emails/transfer_request-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name %} +[{{project}}] Project ownership transfer request +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_start-body-html.jinja b/taiga/projects/templates/emails/transfer_start-body-html.jinja new file mode 100644 index 00000000..06cb70a6 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_start-body-html.jinja @@ -0,0 +1,23 @@ +{% extends "emails/base-body-html.jinja" %} + +{% block body %} + {% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %} +

Hi {{receiver_name}},

+

{{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.

+ {% endtrans %} + + {% if reason %} + {% trans owner_name=project.owner.get_full_name() %} +

{{ owner_name }} says:

+ {% endtrans %} + +

{{ reason }}

+ {% endif %} + + {% trans %} +

Please, click on "Continue" to either accept or reject this proposal.

+ {% endtrans %} + + {{ _("Continue") }} +{% endblock %} diff --git a/taiga/projects/templates/emails/transfer_start-body-text.jinja b/taiga/projects/templates/emails/transfer_start-body-text.jinja new file mode 100644 index 00000000..1c24ed20 --- /dev/null +++ b/taiga/projects/templates/emails/transfer_start-body-text.jinja @@ -0,0 +1,20 @@ +{% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %} +Hi {{receiver_name}}, +{{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner. +{% endtrans %} + +{% if reason %}{% trans owner_name=project.owner.get_full_name() %}{{ owner_name }} says:{% endtrans %} + +{{ reason }} +{% endif %} + +{% trans %} +Please, go to the following link to either accept or reject this proposal.

+{% endtrans %} + +{{ _("Accept or reject the project ownership transfer:") }} {{ resolve_front_url("project-transfer", project.slug, project.transfer_token) }} + +--- +{% trans %} +The Taiga Team +{% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_start-subject.jinja b/taiga/projects/templates/emails/transfer_start-subject.jinja new file mode 100644 index 00000000..d0e34d3a --- /dev/null +++ b/taiga/projects/templates/emails/transfer_start-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name %} +[{{project}}] Project ownership transfer offer +{% endtrans %} diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 3a1650ea..815560c1 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -29,6 +29,7 @@ from taiga.base import exceptions as exc from taiga.base import response from taiga.base import status from taiga.base.decorators import list_route +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.utils import get_object_or_404 @@ -46,7 +47,7 @@ from . import services class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - ModelCrudViewSet): + BlockedByProjectMixin, ModelCrudViewSet): queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) filter_backends = (filters.CanViewUsFilterBackend, @@ -213,6 +214,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi data = serializer.data project = Project.objects.get(id=data["project_id"]) self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + user_stories = services.create_userstories_in_bulk( data["bulk_stories"], project=project, owner=request.user, status_id=data.get("status_id") or project.default_us_status_id, @@ -230,6 +234,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi project = get_object_or_404(Project, pk=data["project_id"]) self.check_permissions(request, "bulk_update_order", project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + services.update_userstories_order_in_bulk(data["bulk_stories"], project=project, field=order_field) diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index 4ef7d996..58e0a65c 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -82,27 +82,18 @@ def connect_all_userstories_signals(): def disconnect_userstories_signals(): - signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="cached_prev_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_role_points_when_create_or_edit_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_milestone_of_tasks_when_edit_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") - signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="try_to_close_milestone_when_delete_us") - signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="tags_normalization_user_story") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") - signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") + signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") + signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us") + signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") + signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") def disconnect_userstories_custom_attributes_signals(): - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="create_custom_attribute_value_when_create_user_story") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story") def disconnect_all_userstories_signals(): diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 6c561c07..4a046524 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -69,7 +69,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod related_name="user_stories", verbose_name=_("status"), on_delete=models.SET_NULL) is_closed = models.BooleanField(default=False) - points = models.ManyToManyField("projects.Points", null=False, blank=False, + points = models.ManyToManyField("projects.Points", blank=False, related_name="userstories", through="RolePoints", verbose_name=_("points")) @@ -97,7 +97,7 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod verbose_name=_("is client requirement")) team_requirement = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is team requirement")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") generated_from_issue = models.ForeignKey("issues.Issue", null=True, blank=True, on_delete=models.SET_NULL, related_name="generated_user_stories", diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index e5f4af7f..326c99fe 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectOwner, + IsAuthenticated, IsProjectAdmin, AllowAny, IsSuperUser) @@ -38,14 +38,14 @@ class UserStoryPermission(TaigaResourcePermission): class UserStoryVotersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_us') list_perms = HasProjectPerm('view_us') class UserStoryWatchersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_us') list_perms = HasProjectPerm('view_us') diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py index fb33304b..2fce2d60 100644 --- a/taiga/projects/votes/mixins/viewsets.py +++ b/taiga/projects/votes/mixins/viewsets.py @@ -28,10 +28,15 @@ from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_i class VotedResourceMixin: - # Note: Update get_queryset method: - # def get_queryset(self): - # qs = super().get_queryset() - # return self.attach_votes_attrs_to_queryset(qs) + """ + Note: Update get_queryset method: + def get_queryset(self): + qs = super().get_queryset() + return self.attach_votes_attrs_to_queryset(qs) + + - the classes using this mixing must have a method: + def pre_conditions_on_save(self, obj) + """ def attach_votes_attrs_to_queryset(self, queryset): qs = attach_total_voters_to_queryset(queryset) @@ -45,6 +50,7 @@ class VotedResourceMixin: def upvote(self, request, pk=None): obj = self.get_object() self.check_permissions(request, "upvote", obj) + self.pre_conditions_on_save(obj) services.add_vote(obj, user=request.user) return response.Ok() @@ -53,6 +59,7 @@ class VotedResourceMixin: def downvote(self, request, pk=None): obj = self.get_object() self.check_permissions(request, "downvote", obj) + self.pre_conditions_on_save(obj) services.remove_vote(obj, user=request.user) return response.Ok() diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py index 85152cbb..03c8976f 100644 --- a/taiga/projects/votes/models.py +++ b/taiga/projects/votes/models.py @@ -17,7 +17,7 @@ # along with this program. If not, see . from django.conf import settings -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -25,7 +25,7 @@ from django.utils.translation import ugettext_lazy as _ class Votes(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count")) class Meta: @@ -46,7 +46,7 @@ class Votes(models.Model): class Vote(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey("content_type", "object_id") + content_object = GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, related_name="votes", verbose_name=_("user")) created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py index af9ffdfa..78fc94c4 100644 --- a/taiga/projects/votes/serializers.py +++ b/taiga/projects/votes/serializers.py @@ -16,16 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api import serializers -from taiga.base.fields import TagsField +from django.contrib.auth import get_user_model -from taiga.users.models import User -from taiga.users.services import get_photo_or_gravatar_url +from taiga.base.api import serializers class VoterSerializer(serializers.ModelSerializer): full_name = serializers.CharField(source='get_full_name', required=False) class Meta: - model = User + model = get_user_model() fields = ('id', 'username', 'full_name') diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index a88c8d14..ea014233 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -23,6 +23,7 @@ from taiga.base import filters from taiga.base import exceptions as exc from taiga.base import response from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route from taiga.projects.models import Project @@ -38,7 +39,9 @@ from . import permissions from . import serializers -class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): +class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, + BlockedByProjectMixin, ModelCrudViewSet): + model = models.WikiPage serializer_class = serializers.WikiPageSerializer permission_classes = (permissions.WikiPagePermission,) @@ -89,7 +92,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): resource_model = models.WikiPage -class WikiLinkViewSet(ModelCrudViewSet): +class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.WikiLink serializer_class = serializers.WikiLinkSerializer permission_classes = (permissions.WikiLinkPermission,) diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index abbdf44d..c3e20e4e 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -41,7 +41,7 @@ class WikiPage(OCCModelMixin, WatchedModelMixin, models.Model): default=timezone.now) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) - attachments = generic.GenericRelation("attachments.Attachment") + attachments = GenericRelation("attachments.Attachment") _importing = None class Meta: diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py index 2a3c8e3f..d85d56ea 100644 --- a/taiga/projects/wiki/permissions.py +++ b/taiga/projects/wiki/permissions.py @@ -16,12 +16,12 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectOwner, AllowAny, + IsAuthenticated, IsProjectAdmin, AllowAny, IsSuperUser) class WikiPagePermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_wiki_pages') by_slug_perms = HasProjectPerm('view_wiki_pages') @@ -36,14 +36,14 @@ class WikiPagePermission(TaigaResourcePermission): class WikiPageWatchersPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_wiki_pages') list_perms = HasProjectPerm('view_wiki_pages') class WikiLinkPermission(TaigaResourcePermission): - enought_perms = IsProjectOwner() | IsSuperUser() + enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_wiki_links') create_perms = HasProjectPerm('add_wiki_link') diff --git a/taiga/stats/services.py b/taiga/stats/services.py index 26934be3..45ae60e5 100644 --- a/taiga/stats/services.py +++ b/taiga/stats/services.py @@ -14,6 +14,7 @@ from django.apps import apps +from django.contrib.auth import get_user_model from django.db.models import Count from django.db.models import Q from django.utils import timezone @@ -27,7 +28,7 @@ from collections import OrderedDict ########################################################################### def get_users_public_stats(): - model = apps.get_model("users", "User") + model = get_user_model() queryset = model.objects.filter(is_active=True, is_system=False) stats = OrderedDict() diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index e68ba949..82dea784 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -14,7 +14,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - +from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.apps import apps @@ -49,7 +50,7 @@ class TimelineViewSet(ReadOnlyListViewSet): page = self.paginate_queryset(queryset) if page is not None: user_ids = list(set([obj.data.get("user", {}).get("id", None) for obj in page.object_list])) - User = apps.get_model("users", "User") + User = get_user_model() users = {u.id: u for u in User.objects.filter(id__in=user_ids)} for obj in page.object_list: @@ -99,7 +100,7 @@ class TimelineViewSet(ReadOnlyListViewSet): class ProfileTimeline(TimelineViewSet): - content_type = "users.user" + content_type = settings.AUTH_USER_MODEL.lower() permission_classes = (permissions.UserTimelinePermission,) def get_timeline(self, user): @@ -107,7 +108,7 @@ class ProfileTimeline(TimelineViewSet): class UserTimeline(TimelineViewSet): - content_type = "users.user" + content_type = settings.AUTH_USER_MODEL.lower() permission_classes = (permissions.UserTimelinePermission,) def get_timeline(self, user): diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index 898e3aa9..c9df776d 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -17,21 +17,23 @@ from django.apps import AppConfig from django.apps import apps +from django.contrib.auth import get_user_model from django.db.models import signals -from . import signals as handlers -from taiga.projects.history.models import HistoryEntry - class TimelineAppConfig(AppConfig): name = "taiga.timeline" verbose_name = "Timeline" def ready(self): - signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="timeline") + from . import signals as handlers + + signals.post_save.connect(handlers.on_new_history_entry, + sender=apps.get_model("history", "HistoryEntry"), + dispatch_uid="timeline") signals.pre_save.connect(handlers.create_membership_push_to_timeline, sender=apps.get_model("projects", "Membership")) signals.post_delete.connect(handlers.delete_membership_push_to_timeline, sender=apps.get_model("projects", "Membership")) signals.post_save.connect(handlers.create_user_push_to_timeline, - sender=apps.get_model("users", "User")) + sender=get_user_model()) diff --git a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py index fdf8ef49..726664f9 100644 --- a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py +++ b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py @@ -18,24 +18,22 @@ # Examples: # python manage.py rebuild_timeline_for_user_creation --settings=settings.local_timeline -from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist from django.core.management.base import BaseCommand from django.db.models import Model -from django.db import reset_queries from django.test.utils import override_settings from taiga.timeline.service import (_get_impl_key_from_model, _timeline_impl_map, extract_user_info) from taiga.timeline.models import Timeline from taiga.timeline.signals import _push_to_timelines -from taiga.users.models import User from unittest.mock import patch import gc + class BulkCreator(object): def __init__(self): self.timeline_objects = [] @@ -75,7 +73,7 @@ def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, c def generate_timeline(): with patch('taiga.timeline.service._add_to_object_timeline', new=custom_add_to_object_timeline): # Users api wasn't a HistoryResourceMixin so we can't interate on the HistoryEntries in this case - users = User.objects.order_by("date_joined") + users = get_user_model().objects.order_by("date_joined") for user in users.iterator(): print("User:", user.date_joined) extra_data = { @@ -87,6 +85,7 @@ def generate_timeline(): bulk_creator.flush() + class Command(BaseCommand): help = 'Regenerate project timeline' diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py index 5bf7d340..6214d129 100644 --- a/taiga/timeline/management/commands/rebuild_timeline.py +++ b/taiga/timeline/management/commands/rebuild_timeline.py @@ -20,24 +20,17 @@ # python manage.py rebuild_timeline --settings=settings.local_timeline --purge # python manage.py rebuild_timeline --settings=settings.local_timeline --initial_date 2014-10-02 -from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.core.management.base import BaseCommand from django.db.models import Model -from django.db import reset_queries from django.test.utils import override_settings - from taiga.projects.models import Project -from taiga.projects.history import services as history_services -from taiga.projects.history.choices import HistoryType from taiga.projects.history.models import HistoryEntry from taiga.timeline.models import Timeline -from taiga.timeline.service import (_add_to_object_timeline, _get_impl_key_from_model, - _timeline_impl_map, extract_user_info) +from taiga.timeline.service import _get_impl_key_from_model,_timeline_impl_map, extract_user_info from taiga.timeline.signals import on_new_history_entry, _push_to_timelines -from taiga.users.models import User from unittest.mock import patch from optparse import make_option diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index d13db447..73eef400 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -22,7 +22,7 @@ from django.utils import timezone from django.core.exceptions import ValidationError from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes.generic import GenericForeignKey +from django.contrib.contenttypes.fields import GenericForeignKey from taiga.projects.models import Project diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index 882f62bc..d4a1563c 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -16,6 +16,7 @@ # along with this program. If not, see . from django.apps import apps +from django.contrib.auth import get_user_model from django.forms import widgets from taiga.base.api import serializers @@ -37,7 +38,7 @@ class TimelineSerializer(serializers.ModelSerializer): if hasattr(obj, "_prefetched_user"): user = obj._prefetched_user else: - User = apps.get_model("users", "User") + User = get_user_model() userData = obj.data.get("user", None) try: user = User.objects.get(id=userData["id"]) diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index f307fa4a..887688fc 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -16,14 +16,12 @@ # along with this program. If not, see . from django.conf import settings +from django.contrib.auth import get_user_model from django.utils import timezone from django.utils.translation import ugettext as _ from taiga.projects.history import services as history_services -from taiga.projects.models import Project -from taiga.users.models import User from taiga.projects.history.choices import HistoryType -from taiga.projects.notifications import services as notifications_services from taiga.timeline.service import (push_to_timeline, build_user_namespace, build_project_namespace, @@ -93,7 +91,7 @@ def on_new_history_entry(sender, instance, created, **kwargs): elif instance.type == HistoryType.delete: event_type = "delete" - user = User.objects.get(id=instance.user["pk"]) + user = get_user_model().objects.get(id=instance.user["pk"]) values_diff = instance.values_diff _clean_description_fields(values_diff) diff --git a/taiga/urls.py b/taiga/urls.py index 14eaf3a4..23afa70f 100644 --- a/taiga/urls.py +++ b/taiga/urls.py @@ -20,7 +20,6 @@ from django.conf.urls import patterns, include, url from django.contrib import admin from .routers import router -from .contrib_routers import router as contrib_router ############################################## @@ -29,7 +28,6 @@ from .contrib_routers import router as contrib_router urlpatterns = [ url(r'^api/v1/', include(router.urls)), - url(r'^api/v1/', include(contrib_router.urls)), url(r'^api/v1/api-auth/', include('taiga.base.api.urls', namespace='api')), url(r'^admin/', include(admin.site.urls)), ] diff --git a/taiga/users/admin.py b/taiga/users/admin.py index 729d64bf..9d4c9815 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -30,14 +30,14 @@ admin.site.unregister(Group) class RoleAdmin(admin.ModelAdmin): list_display = ["name"] - filter_horizontal = ('permissions',) + filter_horizontal = ("permissions",) def formfield_for_manytomany(self, db_field, request=None, **kwargs): - if db_field.name == 'permissions': - qs = kwargs.get('queryset', db_field.rel.to.objects) + if db_field.name == "permissions": + qs = kwargs.get("queryset", db_field.rel.to.objects) # Avoid a major performance hit resolving permission names which # triggers a content_type load: - kwargs['queryset'] = qs.select_related('content_type') + kwargs["queryset"] = qs.select_related("content_type") return super().formfield_for_manytomany( db_field, request=request, **kwargs) @@ -47,18 +47,21 @@ class RoleAdmin(admin.ModelAdmin): class UserAdmin(DjangoUserAdmin): fieldsets = ( - (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), - (_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}), - (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + (None, {"fields": ("username", "password")}), + (_("Personal info"), {"fields": ("full_name", "email", "bio", "photo")}), + (_("Extra info"), {"fields": ("color", "lang", "timezone", "token", "colorize_tags", + "email_token", "new_email")}), + (_("Permissions"), {"fields": ("is_active", "is_superuser")}), + (_("Restrictions"), {"fields": (("max_private_projects", "max_memberships_private_projects"), + ("max_public_projects", "max_memberships_public_projects"))}), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) form = UserChangeForm add_form = UserCreationForm - list_display = ('username', 'email', 'full_name') - list_filter = ('is_superuser', 'is_active') - search_fields = ('username', 'full_name', 'email') - ordering = ('username',) + list_display = ("username", "email", "full_name") + list_filter = ("is_superuser", "is_active") + search_fields = ("username", "full_name", "email") + ordering = ("username",) filter_horizontal = () class RoleInline(admin.TabularInline): diff --git a/taiga/users/api.py b/taiga/users/api.py index f5d59bd4..96e2742d 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -31,6 +31,7 @@ from taiga.auth.tokens import get_user_for_token from taiga.base.decorators import list_route from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.filters import PermissionBasedFilterBackend from taiga.base.api.utils import get_object_or_404 from taiga.base.filters import MembersFilterBackend @@ -403,7 +404,7 @@ class UsersViewSet(ModelCrudViewSet): ## Role ###################################################### -class RolesViewSet(ModelCrudViewSet): +class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Role serializer_class = serializers.RoleSerializer permission_classes = (permissions.RolesPermission, ) diff --git a/taiga/users/migrations/0015_auto_20160120_1409.py b/taiga/users/migrations/0015_auto_20160120_1409.py new file mode 100644 index 00000000..0cfb0ddc --- /dev/null +++ b/taiga/users/migrations/0015_auto_20160120_1409.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0014_auto_20151005_1357'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='max_private_projects', + field=models.IntegerField(null=True, verbose_name='max number of owned private projects', default=settings.MAX_PRIVATE_PROJECTS_PER_USER, blank=True), + ), + migrations.AddField( + model_name='user', + name='max_public_projects', + field=models.IntegerField(null=True, verbose_name='max number of owned public projects', default=settings.MAX_PUBLIC_PROJECTS_PER_USER, blank=True), + ), + ] diff --git a/taiga/users/migrations/0016_auto_20160204_1050.py b/taiga/users/migrations/0016_auto_20160204_1050.py new file mode 100644 index 00000000..244fcd00 --- /dev/null +++ b/taiga/users/migrations/0016_auto_20160204_1050.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0015_auto_20160120_1409'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='max_memberships_private_projects', + field=models.IntegerField(default=settings.MAX_MEMBERSHIPS_PRIVATE_PROJECTS, blank=True, verbose_name='max number of memberships for each owned private project', null=True), + ), + migrations.AddField( + model_name='user', + name='max_memberships_public_projects', + field=models.IntegerField(default=settings.MAX_MEMBERSHIPS_PUBLIC_PROJECTS, blank=True, verbose_name='max number of memberships for each owned public project', null=True), + ), + ] diff --git a/taiga/users/migrations/0017_auto_20160208_1751.py b/taiga/users/migrations/0017_auto_20160208_1751.py new file mode 100644 index 00000000..224248e1 --- /dev/null +++ b/taiga/users/migrations/0017_auto_20160208_1751.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0016_auto_20160204_1050'), + ] + + operations = [ + migrations.AlterModelOptions( + name='role', + options={'ordering': ['order', 'slug'], 'verbose_name': 'role', 'verbose_name_plural': 'roles'}, + ), + migrations.AlterModelOptions( + name='user', + options={'ordering': ['username'], 'verbose_name': 'user', 'verbose_name_plural': 'users'}, + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index dee7e1a0..e205e583 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -15,18 +15,22 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from importlib import import_module + import random import re -import uuid from django.apps import apps +from django.apps.config import MODELS_MODULE_NAME +from django.conf import settings +from django.contrib.auth.models import UserManager, AbstractBaseUser from django.contrib.contenttypes.models import ContentType +from django.core import validators +from django.core.exceptions import AppRegistryNotReady from django.db import models from django.dispatch import receiver -from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import UserManager, AbstractBaseUser -from django.core import validators from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ from django_pgjson.fields import JsonField from djorm_pgarray.fields import TextArrayField @@ -35,9 +39,45 @@ from taiga.auth.tokens import get_token_for_user from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.files import get_file_path from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING from taiga.projects.notifications.choices import NotifyLevel -from easy_thumbnails.files import get_thumbnailer + +def get_user_model_safe(): + """ + Fetches the user model using the app registry. + This doesn't require that an app with the given app label exists, + which makes it safe to call when the registry is being populated. + All other methods to access models might raise an exception about the + registry not being ready yet. + Raises LookupError if model isn't found. + + Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340 + Ongoing Django issue: https://code.djangoproject.com/ticket/22872 + """ + user_app, user_model = settings.AUTH_USER_MODEL.split('.') + + try: + return apps.get_model(user_app, user_model) + except AppRegistryNotReady: + if apps.apps_ready and not apps.models_ready: + # If this function is called while `apps.populate()` is + # loading models, ensure that the module that defines the + # target model has been imported and try looking the model up + # in the app registry. This effectively emulates + # `from path.to.app.models import Model` where we use + # `Model = get_model('app', 'Model')` instead. + app_config = apps.get_app_config(user_app) + # `app_config.import_models()` cannot be used here because it + # would interfere with `apps.populate()`. + import_module('%s.%s' % (app_config.name, MODELS_MODULE_NAME)) + # In order to account for case-insensitivity of model_name, + # look up the model through a private API of the app registry. + return apps.get_registered_model(user_app, user_model) + else: + # This must be a different case (e.g. the model really doesn't + # exist). We just re-raise the exception. + raise def generate_random_hex_color(): @@ -51,11 +91,11 @@ def get_user_file_path(instance, filename): class PermissionsMixin(models.Model): """ A mixin class that adds the fields and methods necessary to support - Django's Permission model using the ModelBackend. + Django"s Permission model using the ModelBackend. """ - is_superuser = models.BooleanField(_('superuser status'), default=False, - help_text=_('Designates that this user has all permissions without ' - 'explicitly assigning them.')) + is_superuser = models.BooleanField(_("superuser status"), default=False, + help_text=_("Designates that this user has all permissions without " + "explicitly assigning them.")) class Meta: abstract = True @@ -84,25 +124,25 @@ class PermissionsMixin(models.Model): class User(AbstractBaseUser, PermissionsMixin): - username = models.CharField(_('username'), max_length=255, unique=True, - help_text=_('Required. 30 characters or fewer. Letters, numbers and ' - '/./-/_ characters'), + username = models.CharField(_("username"), max_length=255, unique=True, + help_text=_("Required. 30 characters or fewer. Letters, numbers and " + "/./-/_ characters"), validators=[ - validators.RegexValidator(re.compile('^[\w.-]+$'), _('Enter a valid username.'), 'invalid') + validators.RegexValidator(re.compile("^[\w.-]+$"), _("Enter a valid username."), "invalid") ]) - email = models.EmailField(_('email address'), max_length=255, blank=True, unique=True) - is_active = models.BooleanField(_('active'), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.')) + email = models.EmailField(_("email address"), max_length=255, blank=True, unique=True) + is_active = models.BooleanField(_("active"), default=True, + help_text=_("Designates whether this user should be treated as " + "active. Unselect this instead of deleting accounts.")) - full_name = models.CharField(_('full name'), max_length=256, blank=True) + full_name = models.CharField(_("full name"), max_length=256, blank=True) color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color, verbose_name=_("color")) bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography")) photo = models.FileField(upload_to=get_user_file_path, max_length=500, null=True, blank=True, verbose_name=_("photo")) - date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + date_joined = models.DateTimeField(_("date joined"), default=timezone.now) lang = models.CharField(max_length=20, null=True, blank=True, default="", verbose_name=_("default language")) theme = models.CharField(max_length=100, null=True, blank=True, default="", @@ -117,16 +157,33 @@ class User(AbstractBaseUser, PermissionsMixin): email_token = models.CharField(max_length=200, null=True, blank=True, default=None, verbose_name=_("email token")) - new_email = models.EmailField(_('new email address'), null=True, blank=True) + new_email = models.EmailField(_("new email address"), null=True, blank=True) is_system = models.BooleanField(null=False, blank=False, default=False) + + + max_private_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_PRIVATE_PROJECTS_PER_USER, + verbose_name=_("max number of owned private projects")) + max_public_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_PUBLIC_PROJECTS_PER_USER, + verbose_name=_("max number of owned public projects")) + max_memberships_private_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_MEMBERSHIPS_PRIVATE_PROJECTS, + verbose_name=_("max number of memberships for " + "each owned private project")) + max_memberships_public_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_MEMBERSHIPS_PUBLIC_PROJECTS, + verbose_name=_("max number of memberships for " + "each owned public project")) + _cached_memberships = None _cached_liked_ids = None _cached_watched_ids = None _cached_notify_levels = None - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] + USERNAME_FIELD = "username" + REQUIRED_FIELDS = ["email"] objects = UserManager() @@ -134,9 +191,6 @@ class User(AbstractBaseUser, PermissionsMixin): verbose_name = "user" verbose_name_plural = "users" ordering = ["username"] - permissions = ( - ("view_user", "Can view user"), - ) def __str__(self): return self.get_full_name() @@ -226,6 +280,9 @@ class User(AbstractBaseUser, PermissionsMixin): self.save() self.auth_data.all().delete() + #Blocking all owned users + self.owned_projects.update(blocked_code=BLOCKED_BY_OWNER_LEAVING) + class Role(models.Model): name = models.CharField(max_length=200, null=False, blank=False, @@ -256,16 +313,13 @@ class Role(models.Model): verbose_name_plural = "roles" ordering = ["order", "slug"] unique_together = (("slug", "project"),) - permissions = ( - ("view_role", "Can view role"), - ) def __str__(self): return self.name class AuthData(models.Model): - user = models.ForeignKey('users.User', related_name="auth_data") + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="auth_data") key = models.SlugField(max_length=50) value = models.CharField(max_length=300) extra = JsonField() diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py index a00cce86..c884edf1 100644 --- a/taiga/users/permissions.py +++ b/taiga/users/permissions.py @@ -20,7 +20,7 @@ from taiga.base.api.permissions import IsSuperUser from taiga.base.api.permissions import AllowAny from taiga.base.api.permissions import IsAuthenticated from taiga.base.api.permissions import HasProjectPerm -from taiga.base.api.permissions import IsProjectOwner +from taiga.base.api.permissions import IsProjectAdmin from taiga.base.api.permissions import PermissionComponent @@ -54,8 +54,8 @@ class UserPermission(TaigaResourcePermission): class RolesPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 5b381252..d36ba768 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -104,20 +104,36 @@ class UserSerializer(serializers.ModelSerializer): return ContactProjectDetailSerializer(projects, many=True).data class UserAdminSerializer(UserSerializer): + total_private_projects = serializers.SerializerMethodField("get_total_private_projects") + total_public_projects = serializers.SerializerMethodField("get_total_public_projects") + class Meta: model = User # IMPORTANT: Maintain the UserSerializer Meta up to date # with this info (including here the email) fields = ("id", "username", "full_name", "full_name_display", "email", "color", "bio", "lang", "theme", "timezone", "is_active", "photo", - "big_photo") - read_only_fields = ("id", "email") + "big_photo", + "max_private_projects", "max_public_projects", + "max_memberships_private_projects", "max_memberships_public_projects", + "total_private_projects", "total_public_projects") + + read_only_fields = ("id", "email", + "max_private_projects", "max_public_projects", + "max_memberships_private_projects", + "max_memberships_public_projects") + + def get_total_private_projects(self, user): + return user.owned_projects.filter(is_private=True).count() + + def get_total_public_projects(self, user): + return user.owned_projects.filter(is_private=False).count() class UserBasicInfoSerializer(UserSerializer): class Meta: model = User - fields = ("username", "full_name_display","photo", "big_photo", "is_active") + fields = ("username", "full_name_display","photo", "big_photo", "is_active", "id") class RecoverySerializer(serializers.Serializer): @@ -182,6 +198,7 @@ class HighLightedContentSerializer(serializers.Serializer): project_name = serializers.SerializerMethodField("get_project_name") project_slug = serializers.SerializerMethodField("get_project_slug") project_is_private = serializers.SerializerMethodField("get_project_is_private") + project_blocked_code = serializers.CharField() assigned_to_username = serializers.CharField() assigned_to_full_name = serializers.CharField() diff --git a/taiga/users/services.py b/taiga/users/services.py index 55c82ca1..b18e8b36 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -20,6 +20,7 @@ This model contains a domain logic for users application. """ from django.apps import apps +from django.contrib.auth import get_user_model from django.db.models import Q from django.db import connection from django.conf import settings @@ -40,7 +41,7 @@ from .gravatar import get_gravatar_url def get_user_by_username_or_email(username_or_email): - user_model = apps.get_model("users", "User") + user_model = get_user_model() qs = user_model.objects.filter(Q(username__iexact=username_or_email) | Q(email__iexact=username_or_email)) @@ -123,7 +124,7 @@ def get_visible_project_ids(from_user, by_user): #- The to user is the owner member_perm_conditions |= \ Q(project__id__in=by_user_project_ids, role__permissions__contains=required_permissions) |\ - Q(project__id__in=by_user_project_ids, is_owner=True) + Q(project__id__in=by_user_project_ids, is_admin=True) Membership = apps.get_model('projects', 'Membership') #Calculating the user memberships adding the permission filter for the by user @@ -322,7 +323,7 @@ def get_watched_list(for_user, from_user, type=None, q=None): -- BEGIN Basic info: we need to mix info from different tables and denormalize it SELECT entities.*, projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, - projects_project.tags_colors, projects_project.logo, + projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo, users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email FROM ( {userstories_sql} @@ -417,7 +418,7 @@ def get_liked_list(for_user, from_user, type=None, q=None): -- BEGIN Basic info: we need to mix info from different tables and denormalize it SELECT entities.*, projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, - projects_project.tags_colors, projects_project.logo, + projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo, users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email FROM ( {projects_sql} @@ -500,7 +501,7 @@ def get_voted_list(for_user, from_user, type=None, q=None): -- BEGIN Basic info: we need to mix info from different tables and denormalize it SELECT entities.*, projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, - projects_project.tags_colors, projects_project.logo, + projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo, users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email FROM ( {userstories_sql} @@ -572,3 +573,62 @@ def get_voted_list(for_user, from_user, type=None, q=None): dict(zip([col[0] for col in desc], row)) for row in cursor.fetchall() ] + + +def has_available_slot_for_project(user, project, new_members=0): + # TODO: Refactor: Create one service for every type of action and move to project services + # + # - has_available_slot_to_create_new_project() + # - has_available_slot_to_update_this_project() + # - has_available_slot_to_transfer_this_project() + # - has_available_slot_to_import_this_project() + # - has_available_slot_to_add_members_to_this_project() + + (enough, error) = _has_available_slot_for_project_type(user, project) + if not enough: + return (enough, error) + return _has_available_slot_for_project_members(user, project, new_members) + + +def _has_available_slot_for_project_type(user, project): + if project.is_private: + if user.max_private_projects is None: + return (True, None) + + current_private_projects = user.owned_projects.filter(is_private=True).exclude(id=project.id).count() + if current_private_projects < user.max_private_projects: + return (True, None) + + return (False, _("You can't have more private projects")) + + else: + if user.max_public_projects is None: + return (True, None) + + current_public_project = user.owned_projects.filter(is_private=False).exclude(id=project.id).count() + if current_public_project < user.max_public_projects: + return (True, None) + + return (False, _("You can't have more public projects")) + + +def _has_available_slot_for_project_members(user, project, new_members): + current_memberships = max(project.memberships.count(), 1) + + if project.is_private: + if user.max_memberships_private_projects is None: + return (True, None) + + if current_memberships + new_members <= user.max_memberships_private_projects: + return (True, None) + + return (False, _("You have reached your current limit of memberships for private projects")) + + else: + if user.max_memberships_public_projects is None: + return (True, None) + + if current_memberships + new_members <= user.max_memberships_public_projects: + return (True, None) + + return (False, _("You have reached your current limit of memberships for public projects")) diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index b9092f47..a9b8545e 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -15,10 +15,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.utils.translation import ugettext as _ + from taiga.base import filters from taiga.base import response +from taiga.base import exceptions as exc from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.decorators import detail_route @@ -28,7 +32,7 @@ from . import permissions from . import tasks -class WebhookViewSet(ModelCrudViewSet): +class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Webhook serializer_class = serializers.WebhookSerializer permission_classes = (permissions.WebhookPermission,) @@ -39,6 +43,7 @@ class WebhookViewSet(ModelCrudViewSet): def test(self, request, pk=None): webhook = self.get_object() self.check_permissions(request, 'test', webhook) + self.pre_conditions_blocked(webhook) webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key) log = serializers.WebhookLogSerializer(webhooklog) @@ -57,8 +62,9 @@ class WebhookLogViewSet(ModelListViewSet): def resend(self, request, pk=None): webhooklog = self.get_object() self.check_permissions(request, 'resend', webhooklog) - webhook = webhooklog.webhook + if webhook.project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) webhooklog = tasks.resend_webhook(webhook.id, webhook.url, webhook.key, webhooklog.request_data) diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py index f6dda872..a10cd1d2 100644 --- a/taiga/webhooks/apps.py +++ b/taiga/webhooks/apps.py @@ -15,19 +15,22 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps from django.apps import AppConfig from django.db.models import signals -from . import signal_handlers as handlers -from taiga.projects.history.models import HistoryEntry - def connect_webhooks_signals(): - signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="webhooks") + from . import signal_handlers as handlers + signals.post_save.connect(handlers.on_new_history_entry, + sender=apps.get_model("history", "HistoryEntry"), + dispatch_uid="webhooks") + + def disconnect_webhooks_signals(): - signals.post_save.disconnect(sender=HistoryEntry, dispatch_uid="webhooks") + signals.post_save.disconnect(sender=apps.get_model("history", "HistoryEntry"), dispatch_uid="webhooks") class WebhooksAppConfig(AppConfig): diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py index 7dc41eea..bc1cae61 100644 --- a/taiga/webhooks/permissions.py +++ b/taiga/webhooks/permissions.py @@ -15,28 +15,28 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectOwner, +from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectAdmin, AllowAny, PermissionComponent) -from taiga.permissions.service import is_project_owner +from taiga.permissions.service import is_project_admin -class IsWebhookProjectOwner(PermissionComponent): +class IsWebhookProjectAdmin(PermissionComponent): def check_permissions(self, request, view, obj=None): - return is_project_owner(request.user, obj.webhook.project) + return is_project_admin(request.user, obj.webhook.project) class WebhookPermission(TaigaResourcePermission): - retrieve_perms = IsProjectOwner() - create_perms = IsProjectOwner() - update_perms = IsProjectOwner() - partial_update_perms = IsProjectOwner() - destroy_perms = IsProjectOwner() + retrieve_perms = IsProjectAdmin() + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() list_perms = AllowAny() - test_perms = IsProjectOwner() + test_perms = IsProjectAdmin() class WebhookLogPermission(TaigaResourcePermission): - retrieve_perms = IsWebhookProjectOwner() + retrieve_perms = IsWebhookProjectAdmin() list_perms = AllowAny() - resend_perms = IsWebhookProjectOwner() + resend_perms = IsWebhookProjectAdmin() diff --git a/tests/factories.py b/tests/factories.py index d5d6d06b..252ce47a 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -26,6 +26,9 @@ from .utils import DUMMY_BMP_DATA import factory +from taiga.permissions.permissions import MEMBERS_PERMISSIONS + + class Factory(factory.DjangoModelFactory): class Meta: @@ -162,7 +165,7 @@ class WikiAttachmentFactory(Factory): class UserFactory(Factory): class Meta: - model = "users.User" + model = settings.AUTH_USER_MODEL strategy = factory.CREATE_STRATEGY username = factory.Sequence(lambda n: "user{}".format(n)) @@ -555,8 +558,9 @@ def create_membership(**kwargs): defaults = { "project": project, - "user": project.owner, - "role": RoleFactory.create(project=project) + "user": UserFactory.create(), + "role": RoleFactory.create(project=project, + permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) } defaults.update(kwargs) diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py index 520e266a..0395d8b4 100644 --- a/tests/integration/resources_permissions/test_attachment_resources.py +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -5,6 +5,7 @@ from django.test.client import MULTIPART_CONTENT from taiga.base.utils import json from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects import choices as project_choices from taiga.projects.attachments.serializers import AttachmentSerializer from tests import factories as f @@ -47,6 +48,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -68,19 +74,30 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) return m @@ -96,6 +113,9 @@ def data_us(data): m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9) m.private_user_story2_attachment = f.UserStoryAttachmentFactory(project=data.private_project2, content_object=m.private_user_story2) + m.blocked_user_story = f.UserStoryFactory(project=data.blocked_project, ref=13) + m.blocked_user_story_attachment = f.UserStoryAttachmentFactory(project=data.blocked_project, + content_object=m.blocked_user_story) return m @@ -108,6 +128,8 @@ def data_task(data): m.private_task1_attachment = f.TaskAttachmentFactory(project=data.private_project1, content_object=m.private_task1) m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) m.private_task2_attachment = f.TaskAttachmentFactory(project=data.private_project2, content_object=m.private_task2) + m.blocked_task = f.TaskFactory(project=data.blocked_project, ref=14) + m.blocked_task_attachment = f.TaskAttachmentFactory(project=data.blocked_project, content_object=m.blocked_task) return m @@ -120,6 +142,8 @@ def data_issue(data): m.private_issue1_attachment = f.IssueAttachmentFactory(project=data.private_project1, content_object=m.private_issue1) m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) m.private_issue2_attachment = f.IssueAttachmentFactory(project=data.private_project2, content_object=m.private_issue2) + m.blocked_issue = f.IssueFactory(project=data.blocked_project, ref=11) + m.blocked_issue_attachment = f.IssueAttachmentFactory(project=data.blocked_project, content_object=m.blocked_issue) return m @@ -132,6 +156,8 @@ def data_wiki(data): m.private_wiki1_attachment = f.WikiAttachmentFactory(project=data.private_project1, content_object=m.private_wiki1) m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) m.private_wiki2_attachment = f.WikiAttachmentFactory(project=data.private_project2, content_object=m.private_wiki2) + m.blocked_wiki = f.WikiPageFactory(project=data.blocked_project, slug=1) + m.blocked_wiki_attachment = f.WikiAttachmentFactory(project=data.blocked_project, content_object=m.blocked_wiki) return m @@ -139,6 +165,7 @@ def test_user_story_attachment_retrieve(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story2_attachment.pk}) + blocked_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.blocked_user_story_attachment.pk}) users = [ None, @@ -154,12 +181,15 @@ def test_user_story_attachment_retrieve(client, data, data_us): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_attachment_retrieve(client, data, data_task): public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk}) private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk}) private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk}) + blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk}) users = [ None, @@ -175,12 +205,15 @@ def test_task_attachment_retrieve(client, data, data_task): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_attachment_retrieve(client, data, data_issue): public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk}) private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk}) private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk}) + blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk}) users = [ None, @@ -196,12 +229,15 @@ def test_issue_attachment_retrieve(client, data, data_issue): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_wiki_attachment_retrieve(client, data, data_wiki): public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk}) private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk}) private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk}) + blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk}) users = [ None, @@ -217,6 +253,8 @@ def test_wiki_attachment_retrieve(client, data, data_wiki): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_user_story_attachment_update(client, data, data_us): @@ -226,7 +264,8 @@ def test_user_story_attachment_update(client, data, data_us): args=[data_us.private_user_story1_attachment.pk]) private_url2 = reverse("userstory-attachments-detail", args=[data_us.private_user_story2_attachment.pk]) - + blocked_url = reverse("userstory-attachments-detail", + args=[data_us.blocked_user_story_attachment.pk]) users = [ None, data.registered_user, @@ -252,11 +291,16 @@ def test_user_story_attachment_update(client, data, data_us): # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + results = helper_test_http_method(client, "put", blocked_url, attachment_data, users) + # assert results == [401, 403, 403, 400, 400] + assert results == [405, 405, 405, 405, 405] + def test_task_attachment_update(client, data, data_task): public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk}) private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk}) private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk}) + blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk}) users = [ None, @@ -279,12 +323,16 @@ def test_task_attachment_update(client, data, data_task): results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] def test_issue_attachment_update(client, data, data_issue): public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk}) private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk}) private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk}) + blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk}) users = [ None, @@ -307,12 +355,16 @@ def test_issue_attachment_update(client, data, data_issue): results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] def test_wiki_attachment_update(client, data, data_wiki): public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk}) private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk}) private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk}) + blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk}) users = [ None, @@ -335,12 +387,16 @@ def test_wiki_attachment_update(client, data, data_wiki): results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] def test_user_story_attachment_patch(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story2_attachment.pk}) + blocked_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.blocked_user_story_attachment.pk}) users = [ None, @@ -359,12 +415,15 @@ def test_user_story_attachment_patch(client, data, data_us): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) + assert results == [401, 403, 403, 451, 451] def test_task_attachment_patch(client, data, data_task): public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk}) private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk}) private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk}) + blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk}) users = [ None, @@ -383,12 +442,15 @@ def test_task_attachment_patch(client, data, data_task): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) + assert results == [401, 403, 403, 451, 451] def test_issue_attachment_patch(client, data, data_issue): public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk}) private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk}) private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk}) + blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk}) users = [ None, @@ -407,12 +469,15 @@ def test_issue_attachment_patch(client, data, data_issue): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) + assert results == [401, 403, 403, 451, 451] def test_wiki_attachment_patch(client, data, data_wiki): public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk}) private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk}) private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk}) + blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk}) users = [ None, @@ -431,12 +496,15 @@ def test_wiki_attachment_patch(client, data, data_wiki): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) + assert results == [401, 403, 403, 451, 451] def test_user_story_attachment_delete(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) private_url2 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story2_attachment.pk}) + blocked_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.blocked_user_story_attachment.pk}) users = [ None, @@ -451,12 +519,15 @@ def test_user_story_attachment_delete(client, data, data_us): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_task_attachment_delete(client, data, data_task): public_url = reverse('task-attachments-detail', kwargs={"pk": data_task.public_task_attachment.pk}) private_url1 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task1_attachment.pk}) private_url2 = reverse('task-attachments-detail', kwargs={"pk": data_task.private_task2_attachment.pk}) + blocked_url = reverse('task-attachments-detail', kwargs={"pk": data_task.blocked_task_attachment.pk}) users = [ None, @@ -471,12 +542,15 @@ def test_task_attachment_delete(client, data, data_task): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_issue_attachment_delete(client, data, data_issue): public_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.public_issue_attachment.pk}) private_url1 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue1_attachment.pk}) private_url2 = reverse('issue-attachments-detail', kwargs={"pk": data_issue.private_issue2_attachment.pk}) + blocked_url = reverse('issue-attachments-detail', kwargs={"pk": data_issue.blocked_issue_attachment.pk}) users = [ None, @@ -491,12 +565,15 @@ def test_issue_attachment_delete(client, data, data_issue): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_wiki_attachment_delete(client, data, data_wiki): public_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.public_wiki_attachment.pk}) private_url1 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki1_attachment.pk}) private_url2 = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.private_wiki2_attachment.pk}) + blocked_url = reverse('wiki-attachments-detail', kwargs={"pk": data_wiki.blocked_wiki_attachment.pk}) users = [ None, @@ -511,6 +588,8 @@ def test_wiki_attachment_delete(client, data, data_wiki): assert results == [401, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_user_story_attachment_create(client, data, data_us): @@ -536,6 +615,15 @@ def test_user_story_attachment_create(client, data, data_us): after_each_request=_after_each_request_hook) assert results == [401, 403, 403, 201, 201] + attachment_data = {"description": "test", + "object_id": data_us.blocked_user_story_attachment.object_id, + "project": data_us.blocked_user_story_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + assert results == [401, 403, 403, 451, 451] + def test_task_attachment_create(client, data, data_task): url = reverse('task-attachments-list') @@ -560,6 +648,18 @@ def test_task_attachment_create(client, data, data_task): after_each_request=_after_each_request_hook) assert results == [401, 403, 403, 201, 201] + attachment_data = {"description": "test", + "object_id": data_task.blocked_task_attachment.object_id, + "project": data_task.blocked_task_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + assert results == [401, 403, 403, 451, 451] + def test_issue_attachment_create(client, data, data_issue): url = reverse('issue-attachments-list') @@ -585,6 +685,19 @@ def test_issue_attachment_create(client, data, data_issue): assert results == [401, 403, 403, 201, 201] + attachment_data = {"description": "test", + "object_id": data_issue.blocked_issue_attachment.object_id, + "project": data_issue.blocked_issue_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + + assert results == [401, 403, 403, 451, 451] + def test_wiki_attachment_create(client, data, data_wiki): url = reverse('wiki-attachments-list') @@ -610,6 +723,19 @@ def test_wiki_attachment_create(client, data, data_wiki): assert results == [401, 201, 201, 201, 201] + attachment_data = {"description": "test", + "object_id": data_wiki.blocked_wiki_attachment.object_id, + "project": data_wiki.blocked_wiki_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + + assert results == [401, 403, 403, 451, 451] + def test_user_story_attachment_list(client, data, data_us): url = reverse('userstory-attachments-list') @@ -623,7 +749,7 @@ def test_user_story_attachment_list(client, data, data_us): ] results = helper_test_http_method_and_count(client, 'get', url, None, users) - assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)] + assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)] def test_task_attachment_list(client, data, data_task): @@ -638,7 +764,7 @@ def test_task_attachment_list(client, data, data_task): ] results = helper_test_http_method_and_count(client, 'get', url, None, users) - assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)] + assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)] def test_issue_attachment_list(client, data, data_issue): @@ -653,7 +779,7 @@ def test_issue_attachment_list(client, data, data_issue): ] results = helper_test_http_method_and_count(client, 'get', url, None, users) - assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)] + assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)] def test_wiki_attachment_list(client, data, data_wiki): @@ -668,4 +794,4 @@ def test_wiki_attachment_list(client, data, data_wiki): ] results = helper_test_http_method_and_count(client, 'get', url, None, users) - assert results == [(200, 2), (200, 2), (200, 2), (200, 3), (200, 3)] + assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)] diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index 4faf909d..b0991080 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -64,15 +64,15 @@ def data(): f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) return m diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index da0fa833..2c90cbfc 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -19,6 +19,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS) @@ -52,6 +53,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -81,21 +87,37 @@ def data(): role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.blocked_project, + role__permissions=[]) + f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project) m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1) m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2) + m.blocked_issue_ca = f.IssueCustomAttributeFactory(project=m.blocked_project) m.public_issue = f.IssueFactory(project=m.public_project, status__project=m.public_project, @@ -115,10 +137,17 @@ def data(): priority__project=m.private_project2, type__project=m.private_project2, milestone__project=m.private_project2) + m.blocked_issue = f.IssueFactory(project=m.blocked_project, + status__project=m.blocked_project, + severity__project=m.blocked_project, + priority__project=m.blocked_project, + type__project=m.blocked_project, + milestone__project=m.blocked_project) m.public_issue_cav = m.public_issue.custom_attributes_values m.private_issue_cav1 = m.private_issue1.custom_attributes_values m.private_issue_cav2 = m.private_issue2.custom_attributes_values + m.blocked_issue_cav = m.blocked_issue.custom_attributes_values return m @@ -131,6 +160,7 @@ def test_issue_custom_attribute_retrieve(client, data): public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk}) users = [ None, @@ -146,12 +176,15 @@ def test_issue_custom_attribute_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_custom_attribute_create(client, data): public_url = reverse('issue-custom-attributes-list') private1_url = reverse('issue-custom-attributes-list') private2_url = reverse('issue-custom-attributes-list') + blocked_url = reverse('issue-custom-attributes-list') users = [ None, @@ -176,11 +209,17 @@ def test_issue_custom_attribute_create(client, data): results = helper_test_http_method(client, 'post', private2_url, issue_ca_data, users) assert results == [401, 403, 403, 403, 201] + issue_ca_data = {"name": "test-new", "project": data.blocked_project.id} + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'post', private2_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 451] + def test_issue_custom_attribute_update(client, data): public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk}) users = [ None, @@ -208,11 +247,18 @@ def test_issue_custom_attribute_update(client, data): results = helper_test_http_method(client, 'put', private2_url, issue_ca_data, users) assert results == [401, 403, 403, 403, 200] + issue_ca_data = serializers.IssueCustomAttributeSerializer(data.blocked_issue_ca).data + issue_ca_data["name"] = "test" + issue_ca_data = json.dumps(issue_ca_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_ca_data, users) + assert results == [401, 403, 403, 403, 451] + def test_issue_custom_attribute_delete(client, data): public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk}) users = [ None, @@ -228,6 +274,8 @@ def test_issue_custom_attribute_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_issue_custom_attribute_list(client, data): @@ -249,12 +297,12 @@ def test_issue_custom_attribute_list(client, data): client.login(data.project_member_with_perms) response = client.json.get(url) - assert len(response.data) == 3 + assert len(response.data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.json.get(url) - assert len(response.data) == 3 + assert len(response.data) == 4 assert response.status_code == 200 @@ -262,6 +310,7 @@ def test_issue_custom_attribute_patch(client, data): public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk}) private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk}) private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk}) + blocked_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.blocked_issue_ca.pk}) users = [ None, @@ -277,6 +326,8 @@ def test_issue_custom_attribute_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_issue_custom_attribute_action_bulk_update_order(client, data): @@ -311,6 +362,12 @@ def test_issue_custom_attribute_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_issue_custom_attributes": [(1,2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] ######################################################### # Issue Custom Attribute @@ -321,6 +378,7 @@ def test_issue_custom_attributes_values_retrieve(client, data): public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk}) private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk}) private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk}) + blocked_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.blocked_issue.pk}) users = [ None, @@ -336,12 +394,15 @@ def test_issue_custom_attributes_values_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_custom_attributes_values_update(client, data): public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk}) private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk}) private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk}) + blocked_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.blocked_issue.pk}) users = [ None, @@ -369,11 +430,18 @@ def test_issue_custom_attributes_values_update(client, data): results = helper_test_http_method(client, 'put', private_url2, issue_data, users) assert results == [401, 403, 403, 200, 200] + issue_data = serializers.IssueCustomAttributesValuesSerializer(data.blocked_issue_cav).data + issue_data["attributes_values"] = {str(data.blocked_issue_ca.pk): "test"} + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] + def test_issue_custom_attributes_values_patch(client, data): public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk}) private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk}) private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk}) + blocked_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.blocked_issue.pk}) users = [ None, @@ -397,3 +465,8 @@ def test_issue_custom_attributes_values_patch(client, data): "version": data.private_issue2.version}) results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.blocked_issue_ca.pk): "test"}, + "version": data.blocked_issue.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 469efacc..2a6d3974 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -2,6 +2,7 @@ import uuid from django.core.urlresolvers import reverse +from taiga.projects import choices as project_choices from taiga.projects.issues.serializers import IssueSerializer from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from taiga.base.utils import json @@ -51,6 +52,12 @@ def data(): public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + issues_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -72,18 +79,30 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_issue = f.IssueFactory(project=m.public_project, status__project=m.public_project, @@ -103,6 +122,12 @@ def data(): priority__project=m.private_project2, type__project=m.private_project2, milestone__project=m.private_project2) + m.blocked_issue = f.IssueFactory(project=m.blocked_project, + status__project=m.blocked_project, + severity__project=m.blocked_project, + priority__project=m.blocked_project, + type__project=m.blocked_project, + milestone__project=m.blocked_project) return m @@ -111,6 +136,7 @@ def test_issue_retrieve(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -126,12 +152,15 @@ def test_issue_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_update(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -160,6 +189,12 @@ def test_issue_update(client, data): results = helper_test_http_method(client, 'put', private_url2, issue_data, users) assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] + def test_issue_update_with_project_change(client): user1 = f.UserFactory.create() @@ -278,6 +313,7 @@ def test_issue_delete(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -292,6 +328,8 @@ def test_issue_delete(client, data): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_issue_list(client, data): @@ -313,14 +351,14 @@ def test_issue_list(client, data): response = client.get(url) issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 3 + assert len(issues_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 3 + assert len(issues_data) == 4 assert response.status_code == 200 @@ -390,11 +428,24 @@ def test_issue_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users) assert results == [401, 403, 403, 201, 201] + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "severity": data.blocked_project.severities.all()[0].pk, + "priority": data.blocked_project.priorities.all()[0].pk, + "status": data.blocked_project.issue_statuses.all()[0].pk, + "type": data.blocked_project.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + def test_issue_patch(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -417,6 +468,10 @@ def test_issue_patch(client, data): results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + def test_issue_bulk_create(client, data): data.public_issue.project.default_issue_status = f.IssueStatusFactory() @@ -437,6 +492,12 @@ def test_issue_bulk_create(client, data): data.private_issue2.project.default_severity = f.SeverityFactory() data.private_issue2.project.save() + data.blocked_issue.project.default_issue_status = f.IssueStatusFactory() + data.blocked_issue.project.default_issue_type = f.IssueTypeFactory() + data.blocked_issue.project.default_priority = f.PriorityFactory() + data.blocked_issue.project.default_severity = f.SeverityFactory() + data.blocked_issue.project.save() + url = reverse('issues-bulk-create') users = [ @@ -462,11 +523,17 @@ def test_issue_bulk_create(client, data): results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] + bulk_data = json.dumps({"bulk_issues": "test1\ntest2", + "project_id": data.blocked_issue.project.pk}) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] + def test_issue_action_upvote(client, data): public_url = reverse('issues-upvote', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-upvote', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-upvote', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-upvote', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -482,12 +549,15 @@ def test_issue_action_upvote(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_issue_action_downvote(client, data): public_url = reverse('issues-downvote', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-downvote', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-downvote', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-downvote', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -503,12 +573,15 @@ def test_issue_action_downvote(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_issue_voters_list(client, data): public_url = reverse('issue-voters-list', kwargs={"resource_id": data.public_issue.pk}) private_url1 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue1.pk}) private_url2 = reverse('issue-voters-list', kwargs={"resource_id": data.private_issue2.pk}) + blocked_url = reverse('issue-voters-list', kwargs={"resource_id": data.blocked_issue.pk}) users = [ None, @@ -524,6 +597,8 @@ def test_issue_voters_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_voters_retrieve(client, data): @@ -536,6 +611,9 @@ def test_issue_voters_retrieve(client, data): add_vote(data.private_issue2, data.project_owner) private_url2 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue2.pk, "pk": data.project_owner.pk}) + add_vote(data.blocked_issue, data.project_owner) + blocked_url = reverse('issue-voters-detail', kwargs={"resource_id": data.blocked_issue.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -551,13 +629,16 @@ def test_issue_voters_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issues_csv(client, data): url = reverse('issues-csv') csv_public_uuid = data.public_project.issues_csv_uuid csv_private1_uuid = data.private_project1.issues_csv_uuid - csv_private2_uuid = data.private_project1.issues_csv_uuid + csv_private2_uuid = data.private_project2.issues_csv_uuid + csv_blocked_uuid = data.blocked_project.issues_csv_uuid users = [ None, @@ -576,11 +657,15 @@ def test_issues_csv(client, data): results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + def test_issue_action_watch(client, data): public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-watch', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-watch', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -596,12 +681,15 @@ def test_issue_action_watch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_issue_action_unwatch(client, data): public_url = reverse('issues-unwatch', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-unwatch', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-unwatch', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-unwatch', kwargs={"pk": data.blocked_issue.pk}) users = [ None, @@ -617,12 +705,15 @@ def test_issue_action_unwatch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_issue_watchers_list(client, data): public_url = reverse('issue-watchers-list', kwargs={"resource_id": data.public_issue.pk}) private_url1 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue1.pk}) private_url2 = reverse('issue-watchers-list', kwargs={"resource_id": data.private_issue2.pk}) + blocked_url = reverse('issue-watchers-list', kwargs={"resource_id": data.blocked_issue.pk}) users = [ None, @@ -638,6 +729,8 @@ def test_issue_watchers_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_watchers_retrieve(client, data): @@ -650,7 +743,9 @@ def test_issue_watchers_retrieve(client, data): add_watcher(data.private_issue2, data.project_owner) private_url2 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue2.pk, "pk": data.project_owner.pk}) - + add_watcher(data.blocked_issue, data.project_owner) + blocked_url = reverse('issue-watchers-detail', kwargs={"resource_id": data.blocked_issue.pk, + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -665,3 +760,5 @@ def test_issue_watchers_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index 40a8c008..5a6f26a4 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -1,6 +1,8 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json + +from taiga.projects import choices as project_choices from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone from taiga.projects.notifications.services import add_watcher @@ -43,6 +45,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -64,22 +71,35 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_milestone = f.MilestoneFactory(project=m.public_project) m.private_milestone1 = f.MilestoneFactory(project=m.private_project1) m.private_milestone2 = f.MilestoneFactory(project=m.private_project2) + m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project) return m @@ -88,6 +108,7 @@ def test_milestone_retrieve(client, data): public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -103,12 +124,15 @@ def test_milestone_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_milestone_update(client, data): public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -136,11 +160,18 @@ def test_milestone_update(client, data): results = helper_test_http_method(client, 'put', private_url2, milestone_data, users) assert results == [401, 403, 403, 200, 200] + milestone_data = MilestoneSerializer(data.blocked_milestone).data + milestone_data["name"] = "test" + milestone_data = json.dumps(milestone_data) + results = helper_test_http_method(client, 'put', blocked_url, milestone_data, users) + assert results == [401, 403, 403, 451, 451] + def test_milestone_delete(client, data): public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -154,6 +185,8 @@ def test_milestone_delete(client, data): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_milestone_list(client, data): @@ -175,14 +208,14 @@ def test_milestone_list(client, data): response = client.get(url) milestones_data = json.loads(response.content.decode('utf-8')) - assert len(milestones_data) == 3 + assert len(milestones_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) milestones_data = json.loads(response.content.decode('utf-8')) - assert len(milestones_data) == 3 + assert len(milestones_data) == 4 assert response.status_code == 200 @@ -227,11 +260,21 @@ def test_milestone_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete()) assert results == [401, 403, 403, 201, 201] + create_data = json.dumps({ + "name": "test", + "estimated_start": "2014-12-10", + "estimated_finish": "2014-12-24", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Milestone.objects.all().delete()) + assert results == [401, 403, 403, 451, 451] + def test_milestone_patch(client, data): public_url = reverse('milestones-detail', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-detail', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-detail', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-detail', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -253,11 +296,16 @@ def test_milestone_patch(client, data): results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"name": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + def test_milestone_action_stats(client, data): public_url = reverse('milestones-stats', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-stats', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-stats', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-stats', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -276,11 +324,15 @@ def test_milestone_action_stats(client, data): results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + def test_milestone_action_watch(client, data): public_url = reverse('milestones-watch', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-watch', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-watch', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-watch', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -296,12 +348,15 @@ def test_milestone_action_watch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_milestone_action_unwatch(client, data): public_url = reverse('milestones-unwatch', kwargs={"pk": data.public_milestone.pk}) private_url1 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone1.pk}) private_url2 = reverse('milestones-unwatch', kwargs={"pk": data.private_milestone2.pk}) + blocked_url = reverse('milestones-unwatch', kwargs={"pk": data.blocked_milestone.pk}) users = [ None, @@ -317,12 +372,15 @@ def test_milestone_action_unwatch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_milestone_watchers_list(client, data): public_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.public_milestone.pk}) private_url1 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone1.pk}) private_url2 = reverse('milestone-watchers-list', kwargs={"resource_id": data.private_milestone2.pk}) + blocked_url = reverse('milestone-watchers-list', kwargs={"resource_id": data.blocked_milestone.pk}) users = [ None, @@ -338,6 +396,8 @@ def test_milestone_watchers_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_milestone_watchers_retrieve(client, data): @@ -350,6 +410,9 @@ def test_milestone_watchers_retrieve(client, data): add_watcher(data.private_milestone2, data.project_owner) private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk, "pk": data.project_owner.pk}) + add_watcher(data.blocked_milestone, data.project_owner) + blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -365,3 +428,5 @@ def test_milestone_watchers_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py new file mode 100644 index 00000000..8260bd2f --- /dev/null +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -0,0 +1,209 @@ +import uuid + +from django.core.urlresolvers import reverse + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.base.utils import json + +from tests import factories as f +from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from taiga.projects import choices as project_choices +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher +from taiga.projects.occ import OCCResourceMixin + +from unittest import mock + +import pytest +pytestmark = pytest.mark.django_db + + +def setup_module(module): + disconnect_signals() + + +def teardown_module(module): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + return m + + +def test_modules_retrieve(client, data): + public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk}) + private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk}) + private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [404, 404, 404, 403, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [404, 404, 404, 403, 200] + + +def test_modules_update(client, data): + public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk}) + private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk}) + private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + results = helper_test_http_method(client, 'put', public_url, {"att": "test"}, users) + assert results == [405, 405, 405, 405, 405] + + results = helper_test_http_method(client, 'put', private_url1, {"att": "test"}, users) + assert results == [405, 405, 405, 405, 405] + + results = helper_test_http_method(client, 'put', private_url2, {"att": "test"}, users) + assert results == [405, 405, 405, 405, 405] + + results = helper_test_http_method(client, 'put', blocked_url, {"att": "test"}, users) + assert results == [405, 405, 405, 405, 405] + + +def test_modules_delete(client, data): + public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk}) + private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk}) + private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [405, 405, 405, 405] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [405, 405, 405, 405] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [405, 405, 405, 405] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [405, 405, 405, 405] + + +def test_modules_patch(client, data): + public_url = reverse('projects-modules', kwargs={"pk": data.public_project.pk}) + private_url1 = reverse('projects-modules', kwargs={"pk": data.private_project1.pk}) + private_url2 = reverse('projects-modules', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-modules', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 403, 204] + + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 403, 204] + + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [404, 404, 404, 403, 204] + + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [404, 404, 404, 403, 451] diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index c94ec9cc..207889f9 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -1,6 +1,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects import serializers from taiga.users.serializers import RoleSerializer from taiga.permissions.permissions import MEMBERS_PERMISSIONS @@ -34,6 +35,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -61,46 +67,65 @@ def data(): email=m.project_member_without_perms.email, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) m.private_points2 = f.PointsFactory(project=m.private_project2) + m.blocked_points = f.PointsFactory(project=m.blocked_project) m.public_user_story_status = f.UserStoryStatusFactory(project=m.public_project) m.private_user_story_status1 = f.UserStoryStatusFactory(project=m.private_project1) m.private_user_story_status2 = f.UserStoryStatusFactory(project=m.private_project2) + m.blocked_user_story_status = f.UserStoryStatusFactory(project=m.blocked_project) m.public_task_status = f.TaskStatusFactory(project=m.public_project) m.private_task_status1 = f.TaskStatusFactory(project=m.private_project1) m.private_task_status2 = f.TaskStatusFactory(project=m.private_project2) + m.blocked_task_status = f.TaskStatusFactory(project=m.blocked_project) m.public_issue_status = f.IssueStatusFactory(project=m.public_project) m.private_issue_status1 = f.IssueStatusFactory(project=m.private_project1) m.private_issue_status2 = f.IssueStatusFactory(project=m.private_project2) + m.blocked_issue_status = f.IssueStatusFactory(project=m.blocked_project) m.public_issue_type = f.IssueTypeFactory(project=m.public_project) m.private_issue_type1 = f.IssueTypeFactory(project=m.private_project1) m.private_issue_type2 = f.IssueTypeFactory(project=m.private_project2) + m.blocked_issue_type = f.IssueTypeFactory(project=m.blocked_project) m.public_priority = f.PriorityFactory(project=m.public_project) m.private_priority1 = f.PriorityFactory(project=m.private_project1) m.private_priority2 = f.PriorityFactory(project=m.private_project2) + m.blocked_priority = f.PriorityFactory(project=m.blocked_project) m.public_severity = f.SeverityFactory(project=m.public_project) m.private_severity1 = f.SeverityFactory(project=m.private_project1) m.private_severity2 = f.SeverityFactory(project=m.private_project2) + m.blocked_severity = f.SeverityFactory(project=m.blocked_project) m.project_template = m.public_project.creation_template @@ -111,6 +136,7 @@ def test_roles_retrieve(client, data): public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk}) users = [ None, @@ -126,12 +152,15 @@ def test_roles_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_roles_update(client, data): public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk}) users = [ None, @@ -159,11 +188,18 @@ def test_roles_update(client, data): results = helper_test_http_method(client, 'put', private2_url, role_data, users) assert results == [401, 403, 403, 403, 200] + role_data = RoleSerializer(data.blocked_project.roles.all()[0]).data + role_data["name"] = "test" + role_data = json.dumps(role_data) + results = helper_test_http_method(client, 'put', blocked_url, role_data, users) + assert results == [401, 403, 403, 403, 451] + def test_roles_delete(client, data): public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk}) users = [ None, @@ -179,6 +215,8 @@ def test_roles_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_roles_list(client, data): @@ -204,13 +242,13 @@ def test_roles_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 5 + assert len(projects_data) == 7 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 5 + assert len(projects_data) == 7 assert response.status_code == 200 @@ -218,6 +256,7 @@ def test_roles_patch(client, data): public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) private2_url = reverse('roles-detail', kwargs={"pk": data.private_project2.roles.all()[0].pk}) + blocked_url = reverse('roles-detail', kwargs={"pk": data.blocked_project.roles.all()[0].pk}) users = [ None, @@ -233,12 +272,15 @@ def test_roles_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_points_retrieve(client, data): public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk}) users = [ None, @@ -254,12 +296,15 @@ def test_points_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_points_update(client, data): public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk}) users = [ None, @@ -287,11 +332,18 @@ def test_points_update(client, data): results = helper_test_http_method(client, 'put', private2_url, points_data, users) assert results == [401, 403, 403, 403, 200] + points_data = serializers.PointsSerializer(data.blocked_points).data + points_data["name"] = "test" + points_data = json.dumps(points_data) + results = helper_test_http_method(client, 'put', blocked_url, points_data, users) + assert results == [401, 403, 403, 403, 451] + def test_points_delete(client, data): public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk}) users = [ None, @@ -307,6 +359,8 @@ def test_points_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_points_list(client, data): @@ -332,13 +386,13 @@ def test_points_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -346,6 +400,7 @@ def test_points_patch(client, data): public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) private2_url = reverse('points-detail', kwargs={"pk": data.private_points2.pk}) + blocked_url = reverse('points-detail', kwargs={"pk": data.blocked_points.pk}) users = [ None, @@ -361,6 +416,8 @@ def test_points_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_points_action_bulk_update_order(client, data): @@ -395,11 +452,19 @@ def test_points_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_points": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_user_story_status_retrieve(client, data): public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk}) users = [ None, @@ -415,12 +480,15 @@ def test_user_story_status_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_user_story_status_update(client, data): public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk}) users = [ None, @@ -448,11 +516,18 @@ def test_user_story_status_update(client, data): results = helper_test_http_method(client, 'put', private2_url, user_story_status_data, users) assert results == [401, 403, 403, 403, 200] + user_story_status_data = serializers.UserStoryStatusSerializer(data.blocked_user_story_status).data + user_story_status_data["name"] = "test" + user_story_status_data = json.dumps(user_story_status_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_status_data, users) + assert results == [401, 403, 403, 403, 451] + def test_user_story_status_delete(client, data): public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk}) users = [ None, @@ -468,6 +543,9 @@ def test_user_story_status_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + def test_user_story_status_list(client, data): @@ -493,13 +571,13 @@ def test_user_story_status_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -507,6 +585,7 @@ def test_user_story_status_patch(client, data): public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) private2_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status2.pk}) + blocked_url = reverse('userstory-statuses-detail', kwargs={"pk": data.blocked_user_story_status.pk}) users = [ None, @@ -522,6 +601,8 @@ def test_user_story_status_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_user_story_status_action_bulk_update_order(client, data): @@ -556,11 +637,19 @@ def test_user_story_status_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_userstory_statuses": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_task_status_retrieve(client, data): public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk}) users = [ None, @@ -576,12 +665,15 @@ def test_task_status_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_status_update(client, data): public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk}) users = [ None, @@ -609,11 +701,18 @@ def test_task_status_update(client, data): results = helper_test_http_method(client, 'put', private2_url, task_status_data, users) assert results == [401, 403, 403, 403, 200] + task_status_data = serializers.TaskStatusSerializer(data.blocked_task_status).data + task_status_data["name"] = "test" + task_status_data = json.dumps(task_status_data) + results = helper_test_http_method(client, 'put', blocked_url, task_status_data, users) + assert results == [401, 403, 403, 403, 451] + def test_task_status_delete(client, data): public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk}) users = [ None, @@ -629,6 +728,9 @@ def test_task_status_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + def test_task_status_list(client, data): @@ -654,13 +756,13 @@ def test_task_status_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -668,6 +770,7 @@ def test_task_status_patch(client, data): public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) private2_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status2.pk}) + blocked_url = reverse('task-statuses-detail', kwargs={"pk": data.blocked_task_status.pk}) users = [ None, @@ -683,6 +786,8 @@ def test_task_status_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_task_status_action_bulk_update_order(client, data): @@ -717,11 +822,19 @@ def test_task_status_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_task_statuses": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_issue_status_retrieve(client, data): public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk}) users = [ None, @@ -737,12 +850,15 @@ def test_issue_status_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_status_update(client, data): public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk}) users = [ None, @@ -770,11 +886,18 @@ def test_issue_status_update(client, data): results = helper_test_http_method(client, 'put', private2_url, issue_status_data, users) assert results == [401, 403, 403, 403, 200] + issue_status_data = serializers.IssueStatusSerializer(data.blocked_issue_status).data + issue_status_data["name"] = "test" + issue_status_data = json.dumps(issue_status_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_status_data, users) + assert results == [401, 403, 403, 403, 451] + def test_issue_status_delete(client, data): public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk}) users = [ None, @@ -790,6 +913,8 @@ def test_issue_status_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_issue_status_list(client, data): @@ -815,13 +940,13 @@ def test_issue_status_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -829,6 +954,7 @@ def test_issue_status_patch(client, data): public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) private2_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status2.pk}) + blocked_url = reverse('issue-statuses-detail', kwargs={"pk": data.blocked_issue_status.pk}) users = [ None, @@ -844,6 +970,8 @@ def test_issue_status_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_issue_status_action_bulk_update_order(client, data): @@ -878,11 +1006,19 @@ def test_issue_status_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_issue_statuses": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_issue_type_retrieve(client, data): public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk}) users = [ None, @@ -898,12 +1034,15 @@ def test_issue_type_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_issue_type_update(client, data): public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk}) users = [ None, @@ -931,11 +1070,18 @@ def test_issue_type_update(client, data): results = helper_test_http_method(client, 'put', private2_url, issue_type_data, users) assert results == [401, 403, 403, 403, 200] + issue_type_data = serializers.IssueTypeSerializer(data.blocked_issue_type).data + issue_type_data["name"] = "test" + issue_type_data = json.dumps(issue_type_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_type_data, users) + assert results == [401, 403, 403, 403, 451] + def test_issue_type_delete(client, data): public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk}) users = [ None, @@ -951,6 +1097,8 @@ def test_issue_type_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_issue_type_list(client, data): @@ -976,13 +1124,13 @@ def test_issue_type_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -990,6 +1138,7 @@ def test_issue_type_patch(client, data): public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) private2_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type2.pk}) + blocked_url = reverse('issue-types-detail', kwargs={"pk": data.blocked_issue_type.pk}) users = [ None, @@ -1005,6 +1154,8 @@ def test_issue_type_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_issue_type_action_bulk_update_order(client, data): @@ -1039,11 +1190,19 @@ def test_issue_type_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_issue_types": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_priority_retrieve(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk}) users = [ None, @@ -1059,12 +1218,15 @@ def test_priority_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_priority_update(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk}) users = [ None, @@ -1092,11 +1254,17 @@ def test_priority_update(client, data): results = helper_test_http_method(client, 'put', private2_url, priority_data, users) assert results == [401, 403, 403, 403, 200] + priority_data = serializers.PrioritySerializer(data.blocked_priority).data + priority_data["name"] = "test" + priority_data = json.dumps(priority_data) + results = helper_test_http_method(client, 'put', blocked_url, priority_data, users) + assert results == [401, 403, 403, 403, 451] def test_priority_delete(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk}) users = [ None, @@ -1112,6 +1280,8 @@ def test_priority_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_priority_list(client, data): @@ -1137,13 +1307,13 @@ def test_priority_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -1151,6 +1321,7 @@ def test_priority_patch(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) private2_url = reverse('priorities-detail', kwargs={"pk": data.private_priority2.pk}) + blocked_url = reverse('priorities-detail', kwargs={"pk": data.blocked_priority.pk}) users = [ None, @@ -1166,6 +1337,8 @@ def test_priority_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_priority_action_bulk_update_order(client, data): @@ -1200,11 +1373,19 @@ def test_priority_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_priorities": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_severity_retrieve(client, data): public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk}) users = [ None, @@ -1220,12 +1401,15 @@ def test_severity_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_severity_update(client, data): public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk}) users = [ None, @@ -1253,11 +1437,18 @@ def test_severity_update(client, data): results = helper_test_http_method(client, 'put', private2_url, severity_data, users) assert results == [401, 403, 403, 403, 200] + severity_data = serializers.SeveritySerializer(data.blocked_severity).data + severity_data["name"] = "test" + severity_data = json.dumps(severity_data) + results = helper_test_http_method(client, 'put', blocked_url, severity_data, users) + assert results == [401, 403, 403, 403, 451] + def test_severity_delete(client, data): public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk}) users = [ None, @@ -1273,6 +1464,8 @@ def test_severity_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_severity_list(client, data): @@ -1298,13 +1491,13 @@ def test_severity_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 @@ -1312,6 +1505,7 @@ def test_severity_patch(client, data): public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) private2_url = reverse('severities-detail', kwargs={"pk": data.private_severity2.pk}) + blocked_url = reverse('severities-detail', kwargs={"pk": data.blocked_severity.pk}) users = [ None, @@ -1327,6 +1521,8 @@ def test_severity_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_severity_action_bulk_update_order(client, data): @@ -1361,11 +1557,19 @@ def test_severity_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_severities": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + def test_membership_retrieve(client, data): public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk}) users = [ None, @@ -1381,12 +1585,15 @@ def test_membership_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_membership_update(client, data): public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk}) users = [ None, @@ -1414,11 +1621,18 @@ def test_membership_update(client, data): results = helper_test_http_method(client, 'put', private2_url, membership_data, users) assert results == [401, 403, 403, 403, 200] + membership_data = serializers.MembershipSerializer(data.blocked_membership).data + membership_data["token"] = "test" + membership_data = json.dumps(membership_data) + results = helper_test_http_method(client, 'put', blocked_url, membership_data, users) + assert results == [401, 403, 403, 403, 451] + def test_membership_delete(client, data): public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk}) users = [ None, @@ -1434,6 +1648,8 @@ def test_membership_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_membership_list(client, data): @@ -1459,13 +1675,13 @@ def test_membership_list(client, data): client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 8 + assert len(projects_data) == 11 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 8 + assert len(projects_data) == 11 assert response.status_code == 200 @@ -1473,6 +1689,7 @@ def test_membership_patch(client, data): public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) private2_url = reverse('memberships-detail', kwargs={"pk": data.private_membership2.pk}) + blocked_url = reverse('memberships-detail', kwargs={"pk": data.blocked_membership.pk}) users = [ None, @@ -1488,6 +1705,8 @@ def test_membership_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_membership_create(client, data): @@ -1522,6 +1741,13 @@ def test_membership_create(client, data): results = helper_test_http_method(client, 'post', url, membership_data, users) assert results == [401, 403, 403, 403, 201] + membership_data = serializers.MembershipSerializer(data.blocked_membership).data + membership_data["id"] = None + membership_data["email"] = "test4@test.com" + membership_data = json.dumps(membership_data) + results = helper_test_http_method(client, 'post', url, membership_data, users) + assert results == [401, 403, 403, 403, 451] + def test_membership_action_bulk_create(client, data): url = reverse('memberships-bulk-create') @@ -1567,15 +1793,27 @@ def test_membership_action_bulk_create(client, data): results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 403, 200] + bulk_data = { + "project_id": data.blocked_project.id, + "bulk_memberships": [ + {"role_id": data.private_membership2.role.pk, "email": "test1@test.com"}, + {"role_id": data.private_membership2.role.pk, "email": "test2@test.com"}, + ] + } + bulk_data = json.dumps(bulk_data) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 403, 451] def test_membership_action_resend_invitation(client, data): public_invitation = f.InvitationFactory(project=data.public_project, role__project=data.public_project) private_invitation1 = f.InvitationFactory(project=data.private_project1, role__project=data.private_project1) private_invitation2 = f.InvitationFactory(project=data.private_project2, role__project=data.private_project2) + blocked_invitation = f.InvitationFactory(project=data.blocked_project, role__project=data.blocked_project) public_url = reverse('memberships-resend-invitation', kwargs={"pk": public_invitation.pk}) private1_url = reverse('memberships-resend-invitation', kwargs={"pk": private_invitation1.pk}) private2_url = reverse('memberships-resend-invitation', kwargs={"pk": private_invitation2.pk}) + blocked_url = reverse('memberships-resend-invitation', kwargs={"pk": blocked_invitation.pk}) users = [ None, @@ -1594,6 +1832,9 @@ def test_membership_action_resend_invitation(client, data): results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 404, 403, 204] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 404, 403, 451] + def test_project_template_retrieve(client, data): url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 81551b38..9bcd7a9f 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse from django.apps import apps from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects.serializers import ProjectDetailSerializer from taiga.permissions.permissions import MEMBERS_PERMISSIONS @@ -33,6 +34,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) f.RoleFactory(project=m.public_project) @@ -52,18 +58,30 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) ContentType = apps.get_model("contenttypes", "ContentType") Project = apps.get_model("projects", "Project") @@ -76,6 +94,8 @@ def data(): f.LikeFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner) f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms) f.LikeFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner) + f.LikeFactory(content_type=project_ct, object_id=m.blocked_project.pk, user=m.project_member_with_perms) + f.LikeFactory(content_type=project_ct, object_id=m.blocked_project.pk, user=m.project_owner) return m @@ -84,6 +104,7 @@ def test_project_retrieve(client, data): public_url = reverse('projects-detail', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-detail', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -98,15 +119,13 @@ def test_project_retrieve(client, data): assert results == [200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 200, 200] def test_project_update(client, data): url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) - - project_data = ProjectDetailSerializer(data.private_project2).data - project_data["is_private"] = False - - project_data = json.dumps(project_data) + blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -115,12 +134,20 @@ def test_project_update(client, data): data.project_owner ] - results = helper_test_http_method(client, 'put', url, project_data, users) + project_data = ProjectDetailSerializer(data.private_project2).data + project_data["is_private"] = False + results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users) assert results == [401, 403, 403, 200] + project_data = ProjectDetailSerializer(data.blocked_project).data + project_data["is_private"] = False + results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users) + assert results == [401, 403, 403, 451] + def test_project_delete(client, data): url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -131,6 +158,9 @@ def test_project_delete(client, data): results = helper_test_http_method(client, 'delete', url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + def test_project_list(client, data): url = reverse('projects-list') @@ -151,19 +181,20 @@ def test_project_list(client, data): response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 4 assert response.status_code == 200 def test_project_patch(client, data): url = reverse('projects-detail', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-detail', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -172,14 +203,19 @@ def test_project_patch(client, data): data.project_owner ] data = json.dumps({"is_private": False}) + results = helper_test_http_method(client, 'patch', url, data, users) assert results == [401, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, data, users) + assert results == [401, 403, 403, 451] + def test_project_action_stats(client, data): public_url = reverse('projects-stats', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-stats', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-stats', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-stats', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -193,12 +229,15 @@ def test_project_action_stats(client, data): assert results == [200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [404, 404, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [404, 404, 200, 200] def test_project_action_issues_stats(client, data): public_url = reverse('projects-issues-stats', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-issues-stats', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-issues-stats', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -212,12 +251,15 @@ def test_project_action_issues_stats(client, data): assert results == [200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [404, 404, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [404, 404, 200, 200] def test_project_action_like(client, data): public_url = reverse('projects-like', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-like', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-like', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-like', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -231,12 +273,15 @@ def test_project_action_like(client, data): assert results == [401, 200, 200, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 451, 451] def test_project_action_unlike(client, data): public_url = reverse('projects-unlike', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-unlike', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-unlike', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-unlike', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -250,12 +295,15 @@ def test_project_action_unlike(client, data): assert results == [401, 200, 200, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 451, 451] def test_project_fans_list(client, data): public_url = reverse('project-fans-list', kwargs={"resource_id": data.public_project.pk}) private1_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project1.pk}) private2_url = reverse('project-fans-list', kwargs={"resource_id": data.private_project2.pk}) + blocked_url = reverse('project-fans-list', kwargs={"resource_id": data.blocked_project.pk}) users = [ None, @@ -271,6 +319,8 @@ def test_project_fans_list(client, data): assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)] results = helper_test_http_method_and_count(client, 'get', private2_url, None, users) assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)] + results = helper_test_http_method_and_count(client, 'get', blocked_url, None, users) + assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)] def test_project_fans_retrieve(client, data): @@ -280,6 +330,8 @@ def test_project_fans_retrieve(client, data): "pk": data.project_owner.pk}) private2_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project2.pk, "pk": data.project_owner.pk}) + blocked_url = reverse('project-fans-detail', kwargs={"resource_id": data.blocked_project.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -295,12 +347,15 @@ def test_project_fans_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_project_watchers_list(client, data): public_url = reverse('project-watchers-list', kwargs={"resource_id": data.public_project.pk}) private1_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project1.pk}) private2_url = reverse('project-watchers-list', kwargs={"resource_id": data.private_project2.pk}) + blocked_url = reverse('project-watchers-list', kwargs={"resource_id": data.blocked_project.pk}) users = [ None, @@ -316,6 +371,8 @@ def test_project_watchers_list(client, data): assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] results = helper_test_http_method_and_count(client, 'get', private2_url, None, users) assert results == [(401, 0), (403, 0), (403, 0), (200, 3), (200, 3)] + results = helper_test_http_method_and_count(client, 'get', blocked_url, None, users) + assert results == [(401, 0), (403, 0), (403, 0), (200, 3), (200, 3)] def test_project_watchers_retrieve(client, data): @@ -325,6 +382,8 @@ def test_project_watchers_retrieve(client, data): "pk": data.project_owner.pk}) private2_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project2.pk, "pk": data.project_owner.pk}) + blocked_url = reverse('project-watchers-detail', kwargs={"resource_id": data.blocked_project.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -340,6 +399,8 @@ def test_project_watchers_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_project_action_create_template(client, data): @@ -401,6 +462,7 @@ def test_regenerate_userstories_csv_uuid(client, data): public_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -417,11 +479,15 @@ def test_regenerate_userstories_csv_uuid(client, data): results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 403, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 403, 451] + def test_regenerate_tasks_csv_uuid(client, data): public_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -438,11 +504,15 @@ def test_regenerate_tasks_csv_uuid(client, data): results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 403, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 403, 451] + def test_regenerate_issues_csv_uuid(client, data): public_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -459,11 +529,15 @@ def test_regenerate_issues_csv_uuid(client, data): results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 403, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 403, 451] + def test_project_action_watch(client, data): public_url = reverse('projects-watch', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-watch', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-watch', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-watch', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -477,12 +551,15 @@ def test_project_action_watch(client, data): assert results == [401, 200, 200, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 451, 451] def test_project_action_unwatch(client, data): public_url = reverse('projects-unwatch', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-unwatch', kwargs={"pk": data.private_project1.pk}) private2_url = reverse('projects-unwatch', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-unwatch', kwargs={"pk": data.blocked_project.pk}) users = [ None, @@ -496,6 +573,8 @@ def test_project_action_unwatch(client, data): assert results == [401, 200, 200, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 451, 451] def test_project_list_with_discover_mode_enabled(client, data): diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index 4f5f4f54..f878ca14 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -66,15 +66,15 @@ def data(): f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) m.view_only_membership = f.MembershipFactory(project=m.private_project2, user=m.other_user, diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index 5bca5dc5..8d3d9442 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -63,15 +63,15 @@ def data(): f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) m.public_issue = f.IssueFactory(project=m.public_project, status__project=m.public_project, diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index 05b5f49d..1fd33e46 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -19,6 +19,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS) @@ -52,6 +53,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -81,21 +87,37 @@ def data(): role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.blocked_project, + role__permissions=[]) + f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project) m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1) m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2) + m.blocked_task_ca = f.TaskCustomAttributeFactory(project=m.blocked_project) m.public_task = f.TaskFactory(project=m.public_project, status__project=m.public_project, @@ -109,10 +131,15 @@ def data(): status__project=m.private_project2, milestone__project=m.private_project2, user_story__project=m.private_project2) + m.blocked_task = f.TaskFactory(project=m.blocked_project, + status__project=m.blocked_project, + milestone__project=m.blocked_project, + user_story__project=m.blocked_project) m.public_task_cav = m.public_task.custom_attributes_values m.private_task_cav1 = m.private_task1.custom_attributes_values m.private_task_cav2 = m.private_task2.custom_attributes_values + m.blocked_task_cav = m.blocked_task.custom_attributes_values return m @@ -125,6 +152,7 @@ def test_task_custom_attribute_retrieve(client, data): public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk}) users = [ None, @@ -140,12 +168,15 @@ def test_task_custom_attribute_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_custom_attribute_create(client, data): public_url = reverse('task-custom-attributes-list') private1_url = reverse('task-custom-attributes-list') private2_url = reverse('task-custom-attributes-list') + blocked_url = reverse('task-custom-attributes-list') users = [ None, @@ -170,11 +201,17 @@ def test_task_custom_attribute_create(client, data): results = helper_test_http_method(client, 'post', private2_url, task_ca_data, users) assert results == [401, 403, 403, 403, 201] + task_ca_data = {"name": "test-new", "project": data.blocked_project.id} + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'post', blocked_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 451] + def test_task_custom_attribute_update(client, data): public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk}) users = [ None, @@ -202,11 +239,18 @@ def test_task_custom_attribute_update(client, data): results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users) assert results == [401, 403, 403, 403, 200] + task_ca_data = serializers.TaskCustomAttributeSerializer(data.blocked_task_ca).data + task_ca_data["name"] = "test" + task_ca_data = json.dumps(task_ca_data) + results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users) + assert results == [401, 403, 403, 403, 451] + def test_task_custom_attribute_delete(client, data): public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk}) users = [ None, @@ -222,6 +266,9 @@ def test_task_custom_attribute_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + def test_task_custom_attribute_list(client, data): @@ -243,12 +290,12 @@ def test_task_custom_attribute_list(client, data): client.login(data.project_member_with_perms) response = client.json.get(url) - assert len(response.data) == 3 + assert len(response.data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.json.get(url) - assert len(response.data) == 3 + assert len(response.data) == 4 assert response.status_code == 200 @@ -256,6 +303,7 @@ def test_task_custom_attribute_patch(client, data): public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk}) private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk}) private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk}) + blocked_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.blocked_task_ca.pk}) users = [ None, @@ -271,6 +319,8 @@ def test_task_custom_attribute_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_task_custom_attribute_action_bulk_update_order(client, data): @@ -305,6 +355,12 @@ def test_task_custom_attribute_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_task_custom_attributes": [(1,2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] ######################################################### # Task Custom Attribute @@ -315,6 +371,7 @@ def test_task_custom_attributes_values_retrieve(client, data): public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk}) private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk}) private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk}) + blocked_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.blocked_task.pk}) users = [ None, @@ -330,12 +387,15 @@ def test_task_custom_attributes_values_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_custom_attributes_values_update(client, data): public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk}) private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk}) private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk}) + blocked_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.blocked_task.pk}) users = [ None, @@ -363,11 +423,18 @@ def test_task_custom_attributes_values_update(client, data): results = helper_test_http_method(client, 'put', private_url2, task_data, users) assert results == [401, 403, 403, 200, 200] + task_data = serializers.TaskCustomAttributesValuesSerializer(data.blocked_task_cav).data + task_data["attributes_values"] = {str(data.blocked_task_ca.pk): "test"} + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] + def test_task_custom_attributes_values_patch(client, data): public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk}) private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk}) private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk}) + blocked_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.blocked_task.pk}) users = [ None, @@ -391,3 +458,8 @@ def test_task_custom_attributes_values_patch(client, data): "version": data.private_task2.version}) results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.blocked_task_ca.pk): "test"}, + "version": data.blocked_task.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 4a871e8e..4771d12c 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -3,6 +3,7 @@ import uuid from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects.tasks.serializers import TaskSerializer from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from taiga.projects.occ import OCCResourceMixin @@ -51,6 +52,12 @@ def data(): public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + tasks_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -72,22 +79,35 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) milestone_public_task = f.MilestoneFactory(project=m.public_project) milestone_private_task1 = f.MilestoneFactory(project=m.private_project1) milestone_private_task2 = f.MilestoneFactory(project=m.private_project2) + milestone_blocked_task = f.MilestoneFactory(project=m.blocked_project) m.public_task = f.TaskFactory(project=m.public_project, status__project=m.public_project, @@ -104,6 +124,11 @@ def data(): milestone=milestone_private_task2, user_story__project=m.private_project2, user_story__milestone=milestone_private_task2) + m.blocked_task = f.TaskFactory(project=m.blocked_project, + status__project=m.blocked_project, + milestone=milestone_blocked_task, + user_story__project=m.blocked_project, + user_story__milestone=milestone_blocked_task) m.public_project.default_task_status = m.public_task.status m.public_project.save() @@ -111,6 +136,8 @@ def data(): m.private_project1.save() m.private_project2.default_task_status = m.private_task2.status m.private_project2.save() + m.blocked_project.default_task_status = m.blocked_task.status + m.blocked_project.save() return m @@ -119,6 +146,7 @@ def test_task_retrieve(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -134,12 +162,15 @@ def test_task_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_update(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -168,6 +199,12 @@ def test_task_update(client, data): results = helper_test_http_method(client, 'put', private_url2, task_data, users) assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.blocked_task).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] + def test_task_update_with_project_change(client): user1 = f.UserFactory.create() @@ -268,6 +305,7 @@ def test_task_delete(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -281,6 +319,8 @@ def test_task_delete(client, data): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_task_list(client, data): @@ -302,14 +342,14 @@ def test_task_list(client, data): response = client.get(url) tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 3 + assert len(tasks_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 3 + assert len(tasks_data) == 4 assert response.status_code == 200 @@ -351,11 +391,21 @@ def test_task_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users) assert results == [401, 403, 403, 201, 201] + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "status": data.blocked_project.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + def test_task_patch(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -378,6 +428,10 @@ def test_task_patch(client, data): results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.blocked_task.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + def test_task_action_bulk_create(client, data): url = reverse('tasks-bulk-create') @@ -417,11 +471,21 @@ def test_task_action_bulk_create(client, data): results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] + bulk_data = json.dumps({ + "bulk_tasks": "test1\ntest2", + "us_id": data.blocked_task.user_story.pk, + "project_id": data.blocked_task.project.pk, + "sprint_id": data.blocked_task.milestone.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] + def test_task_action_upvote(client, data): public_url = reverse('tasks-upvote', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-upvote', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-upvote', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-upvote', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -437,12 +501,15 @@ def test_task_action_upvote(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_task_action_downvote(client, data): public_url = reverse('tasks-downvote', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-downvote', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-downvote', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-downvote', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -458,12 +525,15 @@ def test_task_action_downvote(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_task_voters_list(client, data): public_url = reverse('task-voters-list', kwargs={"resource_id": data.public_task.pk}) private_url1 = reverse('task-voters-list', kwargs={"resource_id": data.private_task1.pk}) private_url2 = reverse('task-voters-list', kwargs={"resource_id": data.private_task2.pk}) + blocked_url = reverse('task-voters-list', kwargs={"resource_id": data.blocked_task.pk}) users = [ None, @@ -479,6 +549,8 @@ def test_task_voters_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_voters_retrieve(client, data): @@ -492,6 +564,10 @@ def test_task_voters_retrieve(client, data): private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk, "pk": data.project_owner.pk}) + add_vote(data.blocked_task, data.project_owner) + blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk, + "pk": data.project_owner.pk}) + users = [ None, data.registered_user, @@ -506,6 +582,8 @@ def test_task_voters_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_tasks_csv(client, data): @@ -513,6 +591,7 @@ def test_tasks_csv(client, data): csv_public_uuid = data.public_project.tasks_csv_uuid csv_private1_uuid = data.private_project1.tasks_csv_uuid csv_private2_uuid = data.private_project1.tasks_csv_uuid + csv_blocked_uuid = data.blocked_project.tasks_csv_uuid users = [ None, @@ -531,11 +610,15 @@ def test_tasks_csv(client, data): results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + def test_task_action_watch(client, data): public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-watch', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-watch', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -551,12 +634,15 @@ def test_task_action_watch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_task_action_unwatch(client, data): public_url = reverse('tasks-unwatch', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-unwatch', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-unwatch', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-unwatch', kwargs={"pk": data.blocked_task.pk}) users = [ None, @@ -572,12 +658,15 @@ def test_task_action_unwatch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_task_watchers_list(client, data): public_url = reverse('task-watchers-list', kwargs={"resource_id": data.public_task.pk}) private_url1 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task1.pk}) private_url2 = reverse('task-watchers-list', kwargs={"resource_id": data.private_task2.pk}) + blocked_url = reverse('task-watchers-list', kwargs={"resource_id": data.blocked_task.pk}) users = [ None, @@ -593,6 +682,8 @@ def test_task_watchers_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_task_watchers_retrieve(client, data): @@ -606,6 +697,9 @@ def test_task_watchers_retrieve(client, data): private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk, "pk": data.project_owner.pk}) + add_watcher(data.blocked_task, data.project_owner) + blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk, + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -620,3 +714,5 @@ def test_task_watchers_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py index e33dc043..0a874443 100644 --- a/tests/integration/resources_permissions/test_timelines_resources.py +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -63,15 +63,15 @@ def data(): f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) return m diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index a665e566..9e6bd6ff 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -19,6 +19,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS) @@ -53,18 +54,23 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, email=m.project_member_with_perms.email, role__project=m.public_project, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, user=m.project_member_with_perms, email=m.project_member_with_perms.email, role__project=m.private_project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, email=m.project_member_without_perms.email, @@ -82,22 +88,37 @@ def data(): role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.blocked_project, + role__permissions=[]) + f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project) m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1) m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2) - + m.blocked_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.blocked_project) m.public_user_story = f.UserStoryFactory(project=m.public_project, status__project=m.public_project) @@ -105,10 +126,13 @@ def data(): status__project=m.private_project1) m.private_user_story2 = f.UserStoryFactory(project=m.private_project2, status__project=m.private_project2) + m.blocked_user_story = f.UserStoryFactory(project=m.blocked_project, + status__project=m.blocked_project) m.public_user_story_cav = m.public_user_story.custom_attributes_values m.private_user_story_cav1 = m.private_user_story1.custom_attributes_values m.private_user_story_cav2 = m.private_user_story2.custom_attributes_values + m.blocked_user_story_cav = m.blocked_user_story.custom_attributes_values return m @@ -121,6 +145,7 @@ def test_userstory_custom_attribute_retrieve(client, data): public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk}) users = [ None, @@ -136,12 +161,15 @@ def test_userstory_custom_attribute_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_userstory_custom_attribute_create(client, data): public_url = reverse('userstory-custom-attributes-list') private1_url = reverse('userstory-custom-attributes-list') private2_url = reverse('userstory-custom-attributes-list') + blocked_url = reverse('userstory-custom-attributes-list') users = [ None, @@ -166,11 +194,17 @@ def test_userstory_custom_attribute_create(client, data): results = helper_test_http_method(client, 'post', private2_url, userstory_ca_data, users) assert results == [401, 403, 403, 403, 201] + userstory_ca_data = {"name": "test-new", "project": data.blocked_project.id} + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'post', blocked_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 451] + def test_userstory_custom_attribute_update(client, data): public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk}) users = [ None, @@ -198,11 +232,18 @@ def test_userstory_custom_attribute_update(client, data): results = helper_test_http_method(client, 'put', private2_url, userstory_ca_data, users) assert results == [401, 403, 403, 403, 200] + userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.blocked_userstory_ca).data + userstory_ca_data["name"] = "test" + userstory_ca_data = json.dumps(userstory_ca_data) + results = helper_test_http_method(client, 'put', blocked_url, userstory_ca_data, users) + assert results == [401, 403, 403, 403, 451] + def test_userstory_custom_attribute_delete(client, data): public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk}) users = [ None, @@ -218,6 +259,8 @@ def test_userstory_custom_attribute_delete(client, data): assert results == [401, 403, 403, 403, 204] results = helper_test_http_method(client, 'delete', private2_url, None, users) assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] def test_userstory_custom_attribute_list(client, data): @@ -239,12 +282,12 @@ def test_userstory_custom_attribute_list(client, data): client.login(data.project_member_with_perms) response = client.json.get(url) - assert len(response.data) == 3 + assert len(response.data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.json.get(url) - assert len(response.data) == 3 + assert len(response.data) == 4 assert response.status_code == 200 @@ -252,6 +295,7 @@ def test_userstory_custom_attribute_patch(client, data): public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk}) private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk}) private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk}) + blocked_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.blocked_userstory_ca.pk}) users = [ None, @@ -267,6 +311,8 @@ def test_userstory_custom_attribute_patch(client, data): assert results == [401, 403, 403, 403, 200] results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] def test_userstory_custom_attribute_action_bulk_update_order(client, data): @@ -301,6 +347,12 @@ def test_userstory_custom_attribute_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 403, 204] + post_data = json.dumps({ + "bulk_userstory_custom_attributes": [(1,2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] ######################################################### @@ -315,6 +367,8 @@ def test_userstory_custom_attributes_values_retrieve(client, data): "user_story_id": data.private_user_story1.pk}) private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={ "user_story_id": data.private_user_story2.pk}) + blocked_url = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.blocked_user_story.pk}) users = [ None, @@ -330,6 +384,8 @@ def test_userstory_custom_attributes_values_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_userstory_custom_attributes_values_update(client, data): @@ -339,7 +395,8 @@ def test_userstory_custom_attributes_values_update(client, data): "user_story_id": data.private_user_story1.pk}) private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={ "user_story_id": data.private_user_story2.pk}) - + blocked_url = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.blocked_user_story.pk}) users = [ None, data.registered_user, @@ -366,6 +423,12 @@ def test_userstory_custom_attributes_values_update(client, data): results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) assert results == [401, 403, 403, 200, 200] + user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.blocked_user_story_cav).data + user_story_data["attributes_values"] = {str(data.blocked_userstory_ca.pk): "test"} + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] + def test_userstory_custom_attributes_values_patch(client, data): public_url = reverse('userstory-custom-attributes-values-detail', kwargs={ @@ -374,7 +437,8 @@ def test_userstory_custom_attributes_values_patch(client, data): "user_story_id": data.private_user_story1.pk}) private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={ "user_story_id": data.private_user_story2.pk}) - + blocked_url = reverse('userstory-custom-attributes-values-detail', kwargs={ + "user_story_id": data.blocked_user_story.pk}) users = [ None, data.registered_user, @@ -397,3 +461,8 @@ def test_userstory_custom_attributes_values_patch(client, data): "version": data.private_user_story2.version}) results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.blocked_userstory_ca.pk): "test"}, + "version": data.blocked_user_story.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 20881aed..80559a87 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -3,6 +3,7 @@ import uuid from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.projects.userstories.serializers import UserStorySerializer from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from taiga.projects.occ import OCCResourceMixin @@ -51,6 +52,12 @@ def data(): public_permissions=[], owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -72,22 +79,35 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) m.private_points2 = f.PointsFactory(project=m.private_project2) + m.blocked_points = f.PointsFactory(project=m.blocked_project) m.public_role_points = f.RolePointsFactory(role=m.public_project.roles.all()[0], points=m.public_points, @@ -104,10 +124,16 @@ def data(): user_story__project=m.private_project2, user_story__milestone__project=m.private_project2, user_story__status__project=m.private_project2) + m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0], + points=m.blocked_points, + user_story__project=m.blocked_project, + user_story__milestone__project=m.blocked_project, + user_story__status__project=m.blocked_project) m.public_user_story = m.public_role_points.user_story m.private_user_story1 = m.private_role_points1.user_story m.private_user_story2 = m.private_role_points2.user_story + m.blocked_user_story = m.blocked_role_points.user_story return m @@ -116,6 +142,7 @@ def test_user_story_retrieve(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -131,12 +158,15 @@ def test_user_story_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_user_story_update(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -165,6 +195,11 @@ def test_user_story_update(client, data): results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] def test_user_story_update_with_project_change(client): user1 = f.UserFactory.create() @@ -265,6 +300,7 @@ def test_user_story_delete(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -278,6 +314,8 @@ def test_user_story_delete(client, data): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_user_story_list(client, data): @@ -299,14 +337,14 @@ def test_user_story_list(client, data): response = client.get(url) userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 3 + assert len(userstories_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 3 + assert len(userstories_data) == 4 assert response.status_code == 200 @@ -333,11 +371,16 @@ def test_user_story_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users) assert results == [401, 403, 403, 201, 201] + create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + def test_user_story_patch(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -360,6 +403,10 @@ def test_user_story_patch(client, data): results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + def test_user_story_action_bulk_create(client, data): url = reverse('userstories-bulk-create') @@ -384,6 +431,10 @@ def test_user_story_action_bulk_create(client, data): results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] + bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.blocked_user_story.project.pk}) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] + def test_user_story_action_bulk_update_order(client, data): url = reverse('userstories-bulk-update-backlog-order') @@ -417,10 +468,19 @@ def test_user_story_action_bulk_update_order(client, data): results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 204, 204] + post_data = json.dumps({ + "bulk_stories": [{"us_id": data.blocked_user_story.id, "order": 2}], + "project_id": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 451, 451] + + def test_user_story_action_upvote(client, data): public_url = reverse('userstories-upvote', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-upvote', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -436,12 +496,15 @@ def test_user_story_action_upvote(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_user_story_action_downvote(client, data): public_url = reverse('userstories-downvote', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-downvote', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-downvote', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -457,12 +520,15 @@ def test_user_story_action_downvote(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_user_story_voters_list(client, data): public_url = reverse('userstory-voters-list', kwargs={"resource_id": data.public_user_story.pk}) private_url1 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story1.pk}) private_url2 = reverse('userstory-voters-list', kwargs={"resource_id": data.private_user_story2.pk}) + blocked_url = reverse('userstory-voters-list', kwargs={"resource_id": data.blocked_user_story.pk}) users = [ None, @@ -478,6 +544,8 @@ def test_user_story_voters_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_user_story_voters_retrieve(client, data): @@ -491,6 +559,9 @@ def test_user_story_voters_retrieve(client, data): private_url2 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story2.pk, "pk": data.project_owner.pk}) + add_vote(data.blocked_user_story, data.project_owner) + blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk, + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -505,6 +576,8 @@ def test_user_story_voters_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_user_stories_csv(client, data): @@ -535,6 +608,7 @@ def test_user_story_action_watch(client, data): public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-watch', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-watch', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -550,12 +624,15 @@ def test_user_story_action_watch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_user_story_action_unwatch(client, data): public_url = reverse('userstories-unwatch', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-unwatch', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-unwatch', kwargs={"pk": data.blocked_user_story.pk}) users = [ None, @@ -571,12 +648,15 @@ def test_user_story_action_unwatch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_userstory_watchers_list(client, data): public_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.public_user_story.pk}) private_url1 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story1.pk}) private_url2 = reverse('userstory-watchers-list', kwargs={"resource_id": data.private_user_story2.pk}) + blocked_url = reverse('userstory-watchers-list', kwargs={"resource_id": data.blocked_user_story.pk}) users = [ None, @@ -592,6 +672,8 @@ def test_userstory_watchers_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_userstory_watchers_retrieve(client, data): @@ -604,6 +686,9 @@ def test_userstory_watchers_retrieve(client, data): add_watcher(data.private_user_story2, data.project_owner) private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk, "pk": data.project_owner.pk}) + add_watcher(data.blocked_user_story, data.project_owner) + blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -619,3 +704,5 @@ def test_userstory_watchers_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py index 63ef52e6..5712b5bc 100644 --- a/tests/integration/resources_permissions/test_webhooks_resources.py +++ b/tests/integration/resources_permissions/test_webhooks_resources.py @@ -1,6 +1,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects import choices as project_choices from taiga.webhooks.serializers import WebhookSerializer from taiga.webhooks.models import Webhook @@ -36,15 +37,25 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) f.MembershipFactory(project=m.project1, user=m.project_owner, - is_owner=True) + is_admin=True) + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.webhook1 = f.WebhookFactory(project=m.project1) m.webhooklog1 = f.WebhookLogFactory(webhook=m.webhook1) m.webhook2 = f.WebhookFactory(project=m.project2) m.webhooklog2 = f.WebhookLogFactory(webhook=m.webhook2) + m.blocked_webhook = f.WebhookFactory(project=m.blocked_project) + m.blocked_webhooklog = f.WebhookLogFactory(webhook=m.blocked_webhook) return m @@ -52,6 +63,7 @@ def data(): def test_webhook_retrieve(client, data): url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk}) url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk}) + blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk}) users = [ None, @@ -63,11 +75,14 @@ def test_webhook_retrieve(client, data): assert results == [401, 403, 200] results = helper_test_http_method(client, 'get', url2, None, users) assert results == [401, 403, 403] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 200] def test_webhook_update(client, data): url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk}) url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk}) + blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk}) users = [ None, @@ -87,10 +102,17 @@ def test_webhook_update(client, data): results = helper_test_http_method(client, 'put', url2, webhook_data, users) assert results == [401, 403, 403] + webhook_data = WebhookSerializer(data.blocked_webhook).data + webhook_data["key"] = "test" + webhook_data = json.dumps(webhook_data) + results = helper_test_http_method(client, 'put', blocked_url, webhook_data, users) + assert results == [401, 403, 451] + def test_webhook_delete(client, data): url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk}) url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk}) + blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk}) users = [ None, @@ -101,6 +123,8 @@ def test_webhook_delete(client, data): assert results == [401, 403, 204] results = helper_test_http_method(client, 'delete', url2, None, users) assert results == [401, 403, 403] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 451] def test_webhook_list(client, data): @@ -122,7 +146,7 @@ def test_webhook_list(client, data): response = client.get(url) webhooks_data = json.loads(response.content.decode('utf-8')) - assert len(webhooks_data) == 1 + assert len(webhooks_data) == 2 assert response.status_code == 200 @@ -153,10 +177,20 @@ def test_webhook_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Webhook.objects.all().delete()) assert results == [401, 403, 403] + create_data = json.dumps({ + "name": "Test", + "url": "http://test.com", + "key": "test", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: Webhook.objects.all().delete()) + assert results == [401, 403, 451] + def test_webhook_patch(client, data): url1 = reverse('webhooks-detail', kwargs={"pk": data.webhook1.pk}) url2 = reverse('webhooks-detail', kwargs={"pk": data.webhook2.pk}) + blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk}) users = [ None, @@ -172,10 +206,15 @@ def test_webhook_patch(client, data): results = helper_test_http_method(client, 'patch', url2, patch_data, users) assert results == [401, 403, 403] + patch_data = json.dumps({"key": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 451] + def test_webhook_action_test(client, data): url1 = reverse('webhooks-test', kwargs={"pk": data.webhook1.pk}) url2 = reverse('webhooks-test', kwargs={"pk": data.webhook2.pk}) + blocked_url = reverse('webhooks-test', kwargs={"pk": data.blocked_webhook.pk}) users = [ None, @@ -193,6 +232,11 @@ def test_webhook_action_test(client, data): assert results == [404, 404, 404] assert _send_request_mock.called is False + with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 451] + assert _send_request_mock.called is False + def test_webhooklogs_list(client, data): url = reverse('webhooklogs-list') @@ -213,13 +257,14 @@ def test_webhooklogs_list(client, data): response = client.get(url) webhooklogs_data = json.loads(response.content.decode('utf-8')) - assert len(webhooklogs_data) == 1 + assert len(webhooklogs_data) == 2 assert response.status_code == 200 def test_webhooklogs_retrieve(client, data): url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk}) url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk}) + blocked_url = reverse('webhooks-detail', kwargs={"pk": data.blocked_webhook.pk}) users = [ None, @@ -233,10 +278,14 @@ def test_webhooklogs_retrieve(client, data): results = helper_test_http_method(client, 'get', url2, None, users) assert results == [401, 403, 403] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 200] + def test_webhooklogs_create(client, data): url1 = reverse('webhooklogs-list') url2 = reverse('webhooklogs-list') + blocked_url = reverse('webhooklogs-list') users = [ None, @@ -250,10 +299,14 @@ def test_webhooklogs_create(client, data): results = helper_test_http_method(client, 'post', url2, None, users) assert results == [405, 405, 405] + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [405, 405, 405] + def test_webhooklogs_delete(client, data): url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk}) url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk}) + blocked_url = reverse('webhooklogs-detail', kwargs={"pk": data.blocked_webhooklog.pk}) users = [ None, @@ -267,10 +320,14 @@ def test_webhooklogs_delete(client, data): results = helper_test_http_method(client, 'delete', url2, None, users) assert results == [405, 405, 405] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [405, 405, 405] + def test_webhooklogs_update(client, data): url1 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog1.pk}) url2 = reverse('webhooklogs-detail', kwargs={"pk": data.webhooklog2.pk}) + blocked_url = reverse('webhooklogs-detail', kwargs={"pk": data.blocked_webhooklog.pk}) users = [ None, @@ -284,16 +341,23 @@ def test_webhooklogs_update(client, data): results = helper_test_http_method(client, 'put', url2, None, users) assert results == [405, 405, 405] + results = helper_test_http_method(client, 'put', blocked_url, None, users) + assert results == [405, 405, 405] + results = helper_test_http_method(client, 'patch', url1, None, users) assert results == [405, 405, 405] results = helper_test_http_method(client, 'patch', url2, None, users) assert results == [405, 405, 405] + results = helper_test_http_method(client, 'patch', blocked_url, None, users) + assert results == [405, 405, 405] + def test_webhooklogs_action_resend(client, data): url1 = reverse('webhooklogs-resend', kwargs={"pk": data.webhooklog1.pk}) url2 = reverse('webhooklogs-resend', kwargs={"pk": data.webhooklog2.pk}) + blocked_url = reverse('webhooklogs-resend', kwargs={"pk": data.blocked_webhooklog.pk}) users = [ None, @@ -306,3 +370,6 @@ def test_webhooklogs_action_resend(client, data): results = helper_test_http_method(client, 'post', url2, None, users) assert results == [404, 404, 404] + + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 451] diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index 14f2f92b..c69fb2bc 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -2,6 +2,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects import choices as project_choices from taiga.projects.notifications.services import add_watcher from taiga.projects.occ import OCCResourceMixin from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer @@ -46,6 +47,11 @@ def data(): anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -67,26 +73,40 @@ def data(): user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) f.MembershipFactory(project=m.public_project, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project1, user=m.project_owner, - is_owner=True) + is_admin=True) f.MembershipFactory(project=m.private_project2, user=m.project_owner, - is_owner=True) + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) m.public_wiki_page = f.WikiPageFactory(project=m.public_project) m.private_wiki_page1 = f.WikiPageFactory(project=m.private_project1) m.private_wiki_page2 = f.WikiPageFactory(project=m.private_project2) + m.blocked_wiki_page = f.WikiPageFactory(project=m.blocked_project) m.public_wiki_link = f.WikiLinkFactory(project=m.public_project) m.private_wiki_link1 = f.WikiLinkFactory(project=m.private_project1) m.private_wiki_link2 = f.WikiLinkFactory(project=m.private_project2) + m.blocked_wiki_link = f.WikiLinkFactory(project=m.blocked_project) return m @@ -95,6 +115,7 @@ def test_wiki_page_retrieve(client, data): public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -110,12 +131,15 @@ def test_wiki_page_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_wiki_page_update(client, data): public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -144,11 +168,18 @@ def test_wiki_page_update(client, data): results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] + def test_wiki_page_delete(client, data): public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -162,6 +193,8 @@ def test_wiki_page_delete(client, data): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_wiki_page_list(client, data): @@ -183,14 +216,14 @@ def test_wiki_page_list(client, data): response = client.get(url) wiki_pages_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_pages_data) == 3 + assert len(wiki_pages_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) wiki_pages_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_pages_data) == 3 + assert len(wiki_pages_data) == 4 assert response.status_code == 200 @@ -229,11 +262,19 @@ def test_wiki_page_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) assert results == [401, 403, 403, 201, 201] + create_data = json.dumps({ + "content": "test", + "slug": "test", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) + assert results == [401, 403, 403, 451, 451] def test_wiki_page_patch(client, data): public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -256,6 +297,10 @@ def test_wiki_page_patch(client, data): results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + def test_wiki_page_action_render(client, data): url = reverse('wiki-render') @@ -277,6 +322,7 @@ def test_wiki_link_retrieve(client, data): public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) users = [ None, @@ -292,12 +338,15 @@ def test_wiki_link_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_wiki_link_update(client, data): public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) users = [ None, @@ -326,11 +375,17 @@ def test_wiki_link_update(client, data): results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users) assert results == [401, 403, 403, 200, 200] + wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users) + assert results == [401, 403, 403, 451, 451] def test_wiki_link_delete(client, data): public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) users = [ None, @@ -344,6 +399,8 @@ def test_wiki_link_delete(client, data): assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] def test_wiki_link_list(client, data): @@ -365,14 +422,14 @@ def test_wiki_link_list(client, data): response = client.get(url) wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 3 + assert len(wiki_links_data) == 4 assert response.status_code == 200 client.login(data.project_owner) response = client.get(url) wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 3 + assert len(wiki_links_data) == 4 assert response.status_code == 200 @@ -411,11 +468,20 @@ def test_wiki_link_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) assert results == [401, 403, 403, 201, 201] + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 451, 451] + def test_wiki_link_patch(client, data): public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) users = [ None, @@ -438,11 +504,16 @@ def test_wiki_link_patch(client, data): results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + def test_wikipage_action_watch(client, data): public_url = reverse('wiki-watch', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-watch', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-watch', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -458,12 +529,15 @@ def test_wikipage_action_watch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_wikipage_action_unwatch(client, data): public_url = reverse('wiki-unwatch', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-unwatch', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-unwatch', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -479,12 +553,15 @@ def test_wikipage_action_unwatch(client, data): assert results == [401, 200, 200, 200, 200] results = helper_test_http_method(client, 'post', private_url2, "", users) assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] def test_wikipage_watchers_list(client, data): public_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.public_wiki_page.pk}) private_url1 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-watchers-list', kwargs={"resource_id": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-watchers-list', kwargs={"resource_id": data.blocked_wiki_page.pk}) users = [ None, @@ -500,6 +577,8 @@ def test_wikipage_watchers_list(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_wikipage_watchers_retrieve(client, data): @@ -512,7 +591,9 @@ def test_wikipage_watchers_retrieve(client, data): add_watcher(data.private_wiki_page2, data.project_owner) private_url2 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page2.pk, "pk": data.project_owner.pk}) - + add_watcher(data.blocked_wiki_page, data.project_owner) + blocked_url = reverse('wiki-watchers-detail', kwargs={"resource_id": data.blocked_wiki_page.pk, + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -527,3 +608,5 @@ def test_wikipage_watchers_retrieve(client, data): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py index 4234cd81..3dee3464 100644 --- a/tests/integration/test_attachments.py +++ b/tests/integration/test_attachments.py @@ -13,7 +13,7 @@ def test_create_user_story_attachment_without_file(client): Bug test "Don't create attachments without attached_file" """ us = f.UserStoryFactory.create() - f.MembershipFactory(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory(project=us.project, user=us.owner, is_admin=True) attachment_data = { "description": "test", "attached_file": None, @@ -30,7 +30,7 @@ def test_create_user_story_attachment_without_file(client): def test_create_attachment_on_wrong_project(client): issue1 = f.create_issue() issue2 = f.create_issue(owner=issue1.owner) - f.MembershipFactory(project=issue1.project, user=issue1.owner, is_owner=True) + f.MembershipFactory(project=issue1.project, user=issue1.owner, is_admin=True) assert issue1.owner == issue2.owner assert issue1.project.owner == issue2.project.owner @@ -49,7 +49,7 @@ def test_create_attachment_on_wrong_project(client): def test_create_attachment_with_long_file_name(client): issue1 = f.create_issue() - f.MembershipFactory(project=issue1.project, user=issue1.owner, is_owner=True) + f.MembershipFactory(project=issue1.project, user=issue1.owner, is_admin=True) url = reverse("issue-attachments-list") diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 2e7a675a..7df2a233 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -34,7 +34,7 @@ def test_issue_custom_attribute_duplicate_name_error_on_create(client): custom_attr_1 = f.IssueCustomAttributeFactory() member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) url = reverse("issue-custom-attributes-list") @@ -51,7 +51,7 @@ def test_issue_custom_attribute_duplicate_name_error_on_update(client): custom_attr_2 = f.IssueCustomAttributeFactory(project=custom_attr_1.project) member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) @@ -67,10 +67,10 @@ def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(cl custom_attr_2 = f.IssueCustomAttributeFactory(name=custom_attr_1.name) member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_2.project, - is_owner=True) + is_admin=True) url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) @@ -94,7 +94,7 @@ def test_issue_custom_attributes_values_update(client): issue = f.IssueFactory() member = f.MembershipFactory(user=issue.project.owner, project=issue.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) ct1_id = "{}".format(custom_attr_1.id) @@ -126,7 +126,7 @@ def test_issue_custom_attributes_values_update_with_error_invalid_key(client): issue = f.IssueFactory() member = f.MembershipFactory(user=issue.project.owner, project=issue.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) ct1_id = "{}".format(custom_attr_1.id) @@ -151,7 +151,7 @@ def test_issue_custom_attributes_values_delete_issue(client): issue = f.IssueFactory() member = f.MembershipFactory(user=issue.project.owner, project=issue.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) ct1_id = "{}".format(custom_attr_1.id) @@ -177,7 +177,7 @@ def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(cli issue = f.IssueFactory() member = f.MembershipFactory(user=issue.project.owner, project=issue.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project) ct1_id = "{}".format(custom_attr_1.id) custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project) diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index 0178c62f..9fb1131c 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -33,7 +33,7 @@ def test_task_custom_attribute_duplicate_name_error_on_create(client): custom_attr_1 = f.TaskCustomAttributeFactory() member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) url = reverse("task-custom-attributes-list") @@ -50,7 +50,7 @@ def test_task_custom_attribute_duplicate_name_error_on_update(client): custom_attr_2 = f.TaskCustomAttributeFactory(project=custom_attr_1.project) member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) @@ -66,10 +66,10 @@ def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(cli custom_attr_2 = f.TaskCustomAttributeFactory(name=custom_attr_1.name) member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_2.project, - is_owner=True) + is_admin=True) url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) @@ -93,7 +93,7 @@ def test_task_custom_attributes_values_update(client): task = f.TaskFactory() member = f.MembershipFactory(user=task.project.owner, project=task.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) ct1_id = "{}".format(custom_attr_1.id) @@ -124,7 +124,7 @@ def test_task_custom_attributes_values_update_with_error_invalid_key(client): task = f.TaskFactory() member = f.MembershipFactory(user=task.project.owner, project=task.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) ct1_id = "{}".format(custom_attr_1.id) @@ -151,7 +151,7 @@ def test_task_custom_attributes_values_delete_task(client): task = f.TaskFactory() member = f.MembershipFactory(user=task.project.owner, project=task.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) ct1_id = "{}".format(custom_attr_1.id) @@ -177,7 +177,7 @@ def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(clien task = f.TaskFactory() member = f.MembershipFactory(user=task.project.owner, project=task.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project) ct1_id = "{}".format(custom_attr_1.id) diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index befda89a..c6a27499 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -33,7 +33,7 @@ def test_userstory_custom_attribute_duplicate_name_error_on_create(client): custom_attr_1 = f.UserStoryCustomAttributeFactory() member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) url = reverse("userstory-custom-attributes-list") @@ -50,7 +50,7 @@ def test_userstory_custom_attribute_duplicate_name_error_on_update(client): custom_attr_2 = f.UserStoryCustomAttributeFactory(project=custom_attr_1.project) member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) @@ -66,10 +66,10 @@ def test_userstory_custom_attribute_duplicate_name_error_on_move_between_project custom_attr_2 = f.UserStoryCustomAttributeFactory(name=custom_attr_1.name) member = f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_1.project, - is_owner=True) + is_admin=True) f.MembershipFactory(user=custom_attr_1.project.owner, project=custom_attr_2.project, - is_owner=True) + is_admin=True) url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) @@ -93,7 +93,7 @@ def test_userstory_custom_attributes_values_update(client): user_story = f.UserStoryFactory() member = f.MembershipFactory(user=user_story.project.owner, project=user_story.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) ct1_id = "{}".format(custom_attr_1.id) @@ -124,7 +124,7 @@ def test_userstory_custom_attributes_values_update_with_error_invalid_key(client user_story = f.UserStoryFactory() member = f.MembershipFactory(user=user_story.project.owner, project=user_story.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) ct1_id = "{}".format(custom_attr_1.id) @@ -155,7 +155,7 @@ def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattri user_story = f.UserStoryFactory() member = f.MembershipFactory(user=user_story.project.owner, project=user_story.project, - is_owner=True) + is_admin=True) custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project) ct1_id = "{}".format(custom_attr_1.id) diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py index 899b1cfb..29578c5f 100644 --- a/tests/integration/test_exporter_api.py +++ b/tests/integration/test_exporter_api.py @@ -43,7 +43,7 @@ def test_valid_project_export_with_celery_disabled(client, settings): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("exporter-detail", args=[project.pk]) @@ -59,7 +59,7 @@ def test_valid_project_export_with_celery_enabled(client, settings): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("exporter-detail", args=[project.pk]) @@ -82,7 +82,7 @@ def test_valid_project_with_throttling(client, settings): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("exporter-detail", args=[project.pk]) diff --git a/tests/integration/test_fan_projects.py b/tests/integration/test_fan_projects.py index 30897948..31e73582 100644 --- a/tests/integration/test_fan_projects.py +++ b/tests/integration/test_fan_projects.py @@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db def test_like_project(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-like", args=(project.id,)) client.login(user) @@ -39,7 +39,7 @@ def test_like_project(client): def test_unlike_project(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-unlike", args=(project.id,)) client.login(user) @@ -51,7 +51,7 @@ def test_unlike_project(client): def test_list_project_fans(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) f.LikeFactory.create(content_object=project, user=user) url = reverse("project-fans-list", args=(project.id,)) @@ -65,7 +65,7 @@ def test_list_project_fans(client): def test_get_project_fan(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) like = f.LikeFactory.create(content_object=project, user=user) url = reverse("project-fans-detail", args=(project.id, like.user.id)) @@ -79,7 +79,7 @@ def test_get_project_fan(client): def test_get_project_is_fan(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url_detail = reverse("projects-detail", args=(project.id,)) url_like = reverse("projects-like", args=(project.id,)) url_unlike = reverse("projects-unlike", args=(project.id,)) diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index 23ea50a0..01469c0a 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -144,7 +144,7 @@ def test_issue_resource_history_test(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) role = f.RoleFactory.create(project=project) - f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) issue = f.IssueFactory.create(owner=user, project=project) mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save" @@ -201,7 +201,7 @@ def test_take_hidden_snapshot(): def test_history_with_only_comment_shouldnot_be_hidden(client): project = f.create_project() us = f.create_userstory(project=project, status__project=project) - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) qs_all = HistoryEntry.objects.all() qs_hidden = qs_all.filter(is_hidden=True) @@ -222,7 +222,7 @@ def test_history_with_only_comment_shouldnot_be_hidden(client): def test_delete_comment_by_project_owner(client): project = f.create_project() us = f.create_userstory(project=project) - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) key = make_key_from_model_object(us) history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, comment="testing", diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index a50b02ca..4494c9f5 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -11,6 +11,7 @@ from taiga.base.utils import json from taiga.hooks.bitbucket import event_hooks from taiga.hooks.bitbucket.api import BitBucketViewSet from taiga.hooks.exceptions import ActionSyntaxException +from taiga.projects import choices as project_choices from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -102,6 +103,26 @@ def test_ok_signature_invalid_network(client): assert "Bad signature" in response.data["_error_message"] +def test_blocked_project(client): + project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF) + f.ProjectModulesConfigFactory(project=project, config={ + "bitbucket": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("bitbucket-hook-list") + url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") + data = json.dumps({"push": {"changes": [{"new": {"target": { "message": "test message"}}}]}}) + response = client.post(url, + data, + content_type="application/json", + HTTP_X_EVENT_KEY="repo:push", + REMOTE_ADDR=settings.BITBUCKET_VALID_ORIGIN_IPS[0]) + + assert response.status_code == 451 + + def test_invalid_ip(client): project = f.ProjectFactory() f.ProjectModulesConfigFactory(project=project, config={ @@ -324,7 +345,7 @@ def test_issues_event_opened_issue(client): issue.project.default_severity = issue.severity issue.project.default_priority = issue.priority issue.project.save() - Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True) + Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_admin=True) notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project) notify_policy.notify_level = NotifyLevel.all notify_policy.save() @@ -530,7 +551,7 @@ def test_issues_event_bad_comment(client): def test_api_get_project_modules(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("projects-modules", args=(project.id,)) @@ -545,7 +566,7 @@ def test_api_get_project_modules(client): def test_api_patch_project_modules(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("projects-modules", args=(project.id,)) diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 5b832643..9bede876 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -9,6 +9,7 @@ from taiga.base.utils import json from taiga.hooks.github import event_hooks from taiga.hooks.github.api import GitHubViewSet from taiga.hooks.exceptions import ActionSyntaxException +from taiga.projects import choices as project_choices from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -53,6 +54,24 @@ def test_ok_signature(client): assert response.status_code == 204 +def test_blocked_project(client): + project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF) + f.ProjectModulesConfigFactory(project=project, config={ + "github": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("github-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data"} + response = client.post(url, json.dumps(data), + HTTP_X_HUB_SIGNATURE="sha1=3c8e83fdaa266f81c036ea0b71e98eb5e054581a", + content_type="application/json") + + assert response.status_code == 451 + + def test_push_event_detected(client): project = f.ProjectFactory() url = reverse("github-hook-list") @@ -241,7 +260,7 @@ def test_issues_event_opened_issue(client): issue.project.default_severity = issue.severity issue.project.default_priority = issue.priority issue.project.save() - Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True) + Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_admin=True) notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project) notify_policy.notify_level = NotifyLevel.all notify_policy.save() @@ -438,7 +457,7 @@ def test_issues_event_bad_comment(client): def test_api_get_project_modules(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("projects-modules", args=(project.id,)) @@ -453,7 +472,7 @@ def test_api_get_project_modules(client): def test_api_patch_project_modules(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("projects-modules", args=(project.id,)) diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index 0f5ee616..14156b37 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -9,6 +9,7 @@ from taiga.base.utils import json from taiga.hooks.gitlab import event_hooks from taiga.hooks.gitlab.api import GitLabViewSet from taiga.hooks.exceptions import ActionSyntaxException +from taiga.projects import choices as project_choices from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -118,6 +119,27 @@ def test_ok_signature_invalid_network(client): assert "Bad signature" in response.data["_error_message"] + +def test_blocked_project(client): + project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF) + f.ProjectModulesConfigFactory(project=project, config={ + "gitlab": { + "secret": "tpnIwJDz4e", + "valid_origin_ips": ["111.111.111.111"], + } + }) + + url = reverse("gitlab-hook-list") + url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") + data = {"test:": "data"} + response = client.post(url, + json.dumps(data), + content_type="application/json", + REMOTE_ADDR="111.111.111.111") + + assert response.status_code == 451 + + def test_invalid_ip(client): project = f.ProjectFactory() f.ProjectModulesConfigFactory(project=project, config={ @@ -386,7 +408,7 @@ def test_issues_event_opened_issue(client): issue.project.default_severity = issue.severity issue.project.default_priority = issue.priority issue.project.save() - Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True) + Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_admin=True) notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project) notify_policy.notify_level = NotifyLevel.all notify_policy.save() @@ -594,7 +616,7 @@ def test_issues_event_bad_comment(client): def test_api_get_project_modules(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("projects-modules", args=(project.id,)) @@ -609,7 +631,7 @@ def test_api_get_project_modules(client): def test_api_patch_project_modules(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("projects-modules", args=(project.id,)) diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index c8573055..159abb76 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -23,6 +23,7 @@ from django.core.urlresolvers import reverse from django.core.files.base import ContentFile from taiga.base.utils import json +from taiga.export_import.dump_service import dict_to_project, TaigaImportError from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue from taiga.projects.userstories.models import UserStory @@ -72,6 +73,88 @@ def test_valid_project_import_without_extra_data(client): assert response_data["watchers"] == [user.email, user_watching.email] +def test_valid_project_without_enough_public_projects_slots(client): + user = f.UserFactory.create(max_public_projects=0) + + url = reverse("importer-list") + data = { + "slug": "public-project-without-slots", + "name": "Imported project", + "description": "Imported project", + "roles": [{"name": "Role"}], + "is_private": False + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "can't have more public projects" in response.data["_error_message"] + assert Project.objects.filter(slug="public-project-without-slots").count() == 0 + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" + + +def test_valid_project_without_enough_private_projects_slots(client): + user = f.UserFactory.create(max_private_projects=0) + + url = reverse("importer-list") + data = { + "slug": "private-project-without-slots", + "name": "Imported project", + "description": "Imported project", + "roles": [{"name": "Role"}], + "is_private": True + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" + assert Project.objects.filter(slug="private-project-without-slots").count() == 0 + + +def test_valid_project_with_enough_public_projects_slots(client): + user = f.UserFactory.create(max_public_projects=1) + + url = reverse("importer-list") + data = { + "slug": "public-project-with-slots", + "name": "Imported project", + "description": "Imported project", + "roles": [{"name": "Role"}], + "is_private": False + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + assert Project.objects.filter(slug="public-project-with-slots").count() == 1 + + +def test_valid_project_with_enough_private_projects_slots(client): + user = f.UserFactory.create(max_private_projects=1) + + url = reverse("importer-list") + data = { + "slug": "private-project-with-slots", + "name": "Imported project", + "description": "Imported project", + "roles": [{"name": "Role"}], + "is_private": True + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + assert Project.objects.filter(slug="private-project-with-slots").count() == 1 + + def test_valid_project_import_with_not_existing_memberships(client): user = f.UserFactory.create() client.login(user) @@ -280,7 +363,7 @@ def test_invalid_project_import_with_custom_attributes(client): def test_invalid_issue_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-issue", args=[project.pk]) @@ -293,7 +376,7 @@ def test_invalid_issue_import(client): def test_valid_user_story_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.save() client.login(user) @@ -314,7 +397,7 @@ def test_valid_user_story_import(client): def test_valid_user_story_import_with_custom_attributes_values(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - membership = f.MembershipFactory(project=project, user=user, is_owner=True) + membership = f.MembershipFactory(project=project, user=user, is_admin=True) project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.save() custom_attr = f.UserStoryCustomAttributeFactory(project=project) @@ -338,7 +421,7 @@ def test_valid_user_story_import_with_custom_attributes_values(client): def test_valid_issue_import_without_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_status = f.IssueStatusFactory.create(project=project) project.default_severity = f.SeverityFactory.create(project=project) @@ -361,7 +444,7 @@ def test_valid_issue_import_without_extra_data(client): def test_valid_issue_import_with_custom_attributes_values(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - membership = f.MembershipFactory(project=project, user=user, is_owner=True) + membership = f.MembershipFactory(project=project, user=user, is_admin=True) project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_status = f.IssueStatusFactory.create(project=project) project.default_severity = f.SeverityFactory.create(project=project) @@ -389,7 +472,7 @@ def test_valid_issue_import_with_extra_data(client): user = f.UserFactory.create() user_watching = f.UserFactory.create(email="testing@taiga.io") project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_status = f.IssueStatusFactory.create(project=project) project.default_severity = f.SeverityFactory.create(project=project) @@ -425,7 +508,7 @@ def test_valid_issue_import_with_extra_data(client): def test_invalid_issue_import_with_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_status = f.IssueStatusFactory.create(project=project) project.default_severity = f.SeverityFactory.create(project=project) @@ -450,7 +533,7 @@ def test_invalid_issue_import_with_extra_data(client): def test_invalid_issue_import_with_bad_choices(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_status = f.IssueStatusFactory.create(project=project) project.default_severity = f.SeverityFactory.create(project=project) @@ -510,7 +593,7 @@ def test_invalid_issue_import_with_bad_choices(client): def test_invalid_us_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-us", args=[project.pk]) @@ -523,7 +606,7 @@ def test_invalid_us_import(client): def test_valid_us_import_without_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.save() client.login(user) @@ -544,7 +627,7 @@ def test_valid_us_import_with_extra_data(client): user = f.UserFactory.create() user_watching = f.UserFactory.create(email="testing@taiga.io") project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.save() client.login(user) @@ -575,7 +658,7 @@ def test_valid_us_import_with_extra_data(client): def test_invalid_us_import_with_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.save() client.login(user) @@ -597,7 +680,7 @@ def test_invalid_us_import_with_extra_data(client): def test_invalid_us_import_with_bad_choices(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_us_status = f.UserStoryStatusFactory.create(project=project) project.save() client.login(user) @@ -618,7 +701,7 @@ def test_invalid_us_import_with_bad_choices(client): def test_invalid_task_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-task", args=[project.pk]) @@ -631,7 +714,7 @@ def test_invalid_task_import(client): def test_valid_task_import_without_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_task_status = f.TaskStatusFactory.create(project=project) project.save() client.login(user) @@ -651,7 +734,7 @@ def test_valid_task_import_without_extra_data(client): def test_valid_task_import_with_custom_attributes_values(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - membership = f.MembershipFactory(project=project, user=user, is_owner=True) + membership = f.MembershipFactory(project=project, user=user, is_admin=True) project.default_task_status = f.TaskStatusFactory.create(project=project) project.save() custom_attr = f.TaskCustomAttributeFactory(project=project) @@ -676,7 +759,7 @@ def test_valid_task_import_with_extra_data(client): user = f.UserFactory.create() user_watching = f.UserFactory.create(email="testing@taiga.io") project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_task_status = f.TaskStatusFactory.create(project=project) project.save() client.login(user) @@ -707,7 +790,7 @@ def test_valid_task_import_with_extra_data(client): def test_invalid_task_import_with_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_task_status = f.TaskStatusFactory.create(project=project) project.save() client.login(user) @@ -729,7 +812,7 @@ def test_invalid_task_import_with_extra_data(client): def test_invalid_task_import_with_bad_choices(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_task_status = f.TaskStatusFactory.create(project=project) project.save() client.login(user) @@ -750,7 +833,7 @@ def test_invalid_task_import_with_bad_choices(client): def test_valid_task_with_user_story(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) project.default_task_status = f.TaskStatusFactory.create(project=project) us = f.UserStoryFactory.create(project=project) project.save() @@ -771,7 +854,7 @@ def test_valid_task_with_user_story(client): def test_invalid_wiki_page_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-wiki-page", args=[project.pk]) @@ -784,7 +867,7 @@ def test_invalid_wiki_page_import(client): def test_valid_wiki_page_import_without_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-wiki-page", args=[project.pk]) @@ -802,7 +885,7 @@ def test_valid_wiki_page_import_with_extra_data(client): user = f.UserFactory.create() user_watching = f.UserFactory.create(email="testing@taiga.io") project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-wiki-page", args=[project.pk]) @@ -830,7 +913,7 @@ def test_valid_wiki_page_import_with_extra_data(client): def test_invalid_wiki_page_import_with_extra_data(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-wiki-page", args=[project.pk]) @@ -850,7 +933,7 @@ def test_invalid_wiki_page_import_with_extra_data(client): def test_invalid_wiki_link_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-wiki-link", args=[project.pk]) @@ -863,7 +946,7 @@ def test_invalid_wiki_link_import(client): def test_valid_wiki_link_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-wiki-link", args=[project.pk]) @@ -881,7 +964,7 @@ def test_valid_wiki_link_import(client): def test_invalid_milestone_import(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-milestone", args=[project.pk]) @@ -895,7 +978,7 @@ def test_valid_milestone_import(client): user = f.UserFactory.create() user_watching = f.UserFactory.create(email="testing@taiga.io") project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-milestone", args=[project.pk]) @@ -913,7 +996,7 @@ def test_valid_milestone_import(client): def test_milestone_import_duplicated_milestone(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory(project=project, user=user, is_owner=True) + f.MembershipFactory(project=project, user=user, is_admin=True) client.login(user) url = reverse("importer-milestone", args=[project.pk]) @@ -930,6 +1013,92 @@ def test_milestone_import_duplicated_milestone(client): assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project" +def test_dict_to_project_with_no_projects_slots_available(client): + user = f.UserFactory.create(max_private_projects=0) + + data = { + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True + } + + with pytest.raises(TaigaImportError) as excinfo: + project = dict_to_project(data, owner=user) + + assert "can't have more private projects" in str(excinfo.value) + + +def test_dict_to_project_with_no_members_private_project_slots_available(client): + user = f.UserFactory.create(max_memberships_private_projects=2) + + data = { + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + } + ] + } + + with pytest.raises(TaigaImportError) as excinfo: + project = dict_to_project(data, owner=user) + + assert "reached your current limit of memberships for private" in str(excinfo.value) + + +def test_dict_to_project_with_no_members_public_project_slots_available(client): + user = f.UserFactory.create(max_memberships_public_projects=2) + + data = { + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + } + ] + } + + with pytest.raises(TaigaImportError) as excinfo: + project = dict_to_project(data, owner=user) + + assert "reached your current limit of memberships for public" in str(excinfo.value) + + def test_invalid_dump_import(client): user = f.UserFactory.create() client.login(user) @@ -957,6 +1126,7 @@ def test_valid_dump_import_with_logo(client, settings): "slug": "valid-project", "name": "Valid project", "description": "Valid project desc", + "is_private": False, "logo": { "name": "logo.bmp", "data": base64.b64encode(DUMMY_BMP_DATA).decode("utf-8") @@ -986,7 +1156,8 @@ def test_valid_dump_import_with_celery_disabled(client, settings): data = ContentFile(bytes(json.dumps({ "slug": "valid-project", "name": "Valid project", - "description": "Valid project desc" + "description": "Valid project desc", + "is_private": True }), "utf-8")) data.name = "test" @@ -1008,7 +1179,8 @@ def test_valid_dump_import_with_celery_enabled(client, settings): data = ContentFile(bytes(json.dumps({ "slug": "valid-project", "name": "Valid project", - "description": "Valid project desc" + "description": "Valid project desc", + "is_private": True }), "utf-8")) data.name = "test" @@ -1028,7 +1200,8 @@ def test_dump_import_duplicated_project(client): data = ContentFile(bytes(json.dumps({ "slug": project.slug, "name": "Test import", - "description": "Valid project desc" + "description": "Valid project desc", + "is_private": True }), "utf-8")) data.name = "test" @@ -1051,7 +1224,8 @@ def test_dump_import_throttling(client, settings): data = ContentFile(bytes(json.dumps({ "slug": project.slug, "name": "Test import", - "description": "Valid project desc" + "description": "Valid project desc", + "is_private": True }), "utf-8")) data.name = "test" @@ -1061,6 +1235,256 @@ def test_dump_import_throttling(client, settings): assert response.status_code == 429 +def test_valid_dump_import_without_enough_public_projects_slots(client): + user = f.UserFactory.create(max_public_projects=0) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "public-project-without-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + assert "can't have more public projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" + assert Project.objects.filter(slug="public-project-without-slots").count() == 0 + + +def test_valid_dump_import_without_enough_private_projects_slots(client): + user = f.UserFactory.create(max_private_projects=0) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "private-project-without-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" + assert Project.objects.filter(slug="private-project-without-slots").count() == 0 + + +def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client): + user = f.UserFactory.create(max_memberships_private_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + { + "email": "test6@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + assert "reached your current limit of memberships for private" in response.data["_error_message"] + assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0 + + +def test_valid_dump_import_without_enough_membership_public_project_slots_one_project(client): + user = f.UserFactory.create(max_memberships_public_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + { + "email": "test6@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + assert "reached your current limit of memberships for public" in response.data["_error_message"] + assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0 + + +def test_valid_dump_import_with_enough_membership_private_project_slots_multiple_projects(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create(max_memberships_private_projects=10) + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + { + "email": "test6@test.com", + "role": "Role", + } + ] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + response_data = response.data + assert "id" in response_data + assert response_data["name"] == "Valid project" + + +def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_projects(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create(max_memberships_public_projects=10) + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + { + "email": "test6@test.com", + "role": "Role", + } + ] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + response_data = response.data + assert "id" in response_data + assert response_data["name"] == "Valid project" + + def test_valid_dump_import_without_slug(client): project = f.ProjectFactory.create(slug="existing-slug") user = f.UserFactory.create() @@ -1077,3 +1501,87 @@ def test_valid_dump_import_without_slug(client): response = client.post(url, {'dump': data}) assert response.status_code == 201 + + +def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_private_project(client): + user = f.UserFactory.create(max_memberships_private_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "private-project-with-memberships-limit-with-you", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "memberships": [ + { + "email": user.email, + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + assert Project.objects.filter(slug="private-project-with-memberships-limit-with-you").count() == 1 + + +def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_public_project(client): + user = f.UserFactory.create(max_memberships_public_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "public-project-with-memberships-limit-with-you", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "memberships": [ + { + "email": user.email, + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + assert Project.objects.filter(slug="public-project-with-memberships-limit-with-you").count() == 1 diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index b24e54fb..71f45f3f 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -58,7 +58,7 @@ def test_create_issue_without_status(client): project.default_severity = severity project.default_issue_type = type project.save() - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("issues-list") data = {"subject": "Test user story", "project": project.id} @@ -79,7 +79,7 @@ def test_create_issue_without_status_in_project_without_default_values(client): default_severity=None, default_issue_type = None) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("issues-list") data = {"subject": "Test user story", "project": project.id} @@ -94,7 +94,7 @@ def test_create_issue_without_status_in_project_without_default_values(client): def test_api_create_issues_in_bulk(client): project = f.create_project() - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("issues-bulk-create") diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index 75d7f29a..c878bd45 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -34,7 +34,7 @@ def test_api_create_bulk_members(client): joseph = f.UserFactory.create() tester = f.RoleFactory(project=project, name="Tester") gamer = f.RoleFactory(project=project, name="Gamer") - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("memberships-bulk-create") @@ -53,10 +53,143 @@ def test_api_create_bulk_members(client): assert response.data[1]["email"] == joseph.email +def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client): + user = f.UserFactory.create(max_memberships_private_projects=3) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached your current limit of memberships for private" in response.data["_error_message"] + + + +def test_api_create_bulk_members_for_admin_without_enough_memberships_private_project_slots_one_project(client): + owner = f.UserFactory.create(max_memberships_private_projects=3) + user = f.UserFactory.create() + project = f.ProjectFactory(owner=owner, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached your current limit of memberships for private" in response.data["_error_message"] + + + +def test_api_create_bulk_members_with_enough_memberships_private_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_memberships_private_projects=6) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + +def test_api_create_bulk_members_without_enough_memberships_public_project_slots_one_project(client): + user = f.UserFactory.create(max_memberships_public_projects=3) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached your current limit of memberships for public" in response.data["_error_message"] + + +def test_api_create_bulk_members_with_enough_memberships_public_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_memberships_public_projects=6) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + def test_api_create_bulk_members_with_extra_text(client, outbox): project = f.ProjectFactory() tester = f.RoleFactory(project=project, name="Tester") - f.MembershipFactory(project=project, user=project.owner, is_owner=True) + f.MembershipFactory(project=project, user=project.owner, is_admin=True) url = reverse("memberships-bulk-create") invitation_extra_text = "this is a not so random invitation text" @@ -81,7 +214,7 @@ def test_api_create_bulk_members_with_extra_text(client, outbox): def test_api_resend_invitation(client, outbox): invitation = f.create_invitation(user=None) - f.MembershipFactory(project=invitation.project, user=invitation.project.owner, is_owner=True) + f.MembershipFactory(project=invitation.project, user=invitation.project.owner, is_admin=True) url = reverse("memberships-resend-invitation", kwargs={"pk": invitation.pk}) client.login(invitation.project.owner) @@ -96,7 +229,7 @@ def test_api_invite_existing_user(client, outbox): "Should create the invitation linked to that user" user = f.UserFactory.create() role = f.RoleFactory.create() - f.MembershipFactory(project=role.project, user=role.project.owner, is_owner=True) + f.MembershipFactory(project=role.project, user=role.project.owner, is_admin=True) client.login(role.project.owner) @@ -149,7 +282,7 @@ def test_api_create_invalid_membership_role_doesnt_exist_in_the_project(client): def test_api_create_membership(client): - membership = f.MembershipFactory(is_owner=True) + membership = f.MembershipFactory(is_admin=True) role = f.RoleFactory.create(project=membership.project) user = f.UserFactory.create() @@ -162,8 +295,78 @@ def test_api_create_membership(client): assert response.data["user_email"] == user.email +def test_api_create_membership_without_enough_memberships_private_project_slots_one_projects(client): + user = f.UserFactory.create(max_memberships_private_projects=1) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached your current limit of memberships for private" in response.data["_error_message"] + + +def test_api_create_membership_with_enough_memberships_private_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_memberships_private_projects=5) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + +def test_api_create_membership_without_enough_memberships_public_project_slots_one_projects(client): + user = f.UserFactory.create(max_memberships_public_projects=1) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached your current limit of memberships for public" in response.data["_error_message"] + + +def test_api_create_membership_with_enough_memberships_public_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_memberships_public_projects=5) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_admin=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + def test_api_edit_membership(client): - membership = f.MembershipFactory(is_owner=True) + membership = f.MembershipFactory(is_admin=True) client.login(membership.user) url = reverse("memberships-detail", args=[membership.id]) data = {"email": "new@email.com"} @@ -171,16 +374,30 @@ def test_api_edit_membership(client): assert response.status_code == 200 +def test_api_change_owner_membership_to_no_admin_return_error(client): + project = f.ProjectFactory() + membership_owner = f.MembershipFactory(project=project, user=project.owner, is_admin=True) + membership = f.MembershipFactory(project=project, is_admin=True) + + url = reverse("memberships-detail", args=[membership_owner.id]) + data = {"is_admin": False} + + client.login(membership.user) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 400 + assert 'is_admin' in response.data + def test_api_delete_membership(client): - membership = f.MembershipFactory(is_owner=True) + membership = f.MembershipFactory(is_admin=True) client.login(membership.user) url = reverse("memberships-detail", args=[membership.id]) response = client.json.delete(url) assert response.status_code == 400 - f.MembershipFactory(is_owner=True, project=membership.project) + f.MembershipFactory(is_admin=True, project=membership.project) url = reverse("memberships-detail", args=[membership.id]) response = client.json.delete(url) @@ -189,7 +406,7 @@ def test_api_delete_membership(client): def test_api_delete_membership_without_user(client): - membership_owner = f.MembershipFactory(is_owner=True) + membership_owner = f.MembershipFactory(is_admin=True) membership_without_user_one = f.MembershipFactory(project=membership_owner.project, user=None) f.MembershipFactory(project=membership_owner.project, user=None) client.login(membership_owner.user) diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index e7d6c2b8..4934c324 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -33,7 +33,7 @@ def test_update_milestone_with_userstories_list(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) role = f.RoleFactory.create(project=project) - f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) sprint = f.MilestoneFactory.create(project=project, owner=user) f.PointsFactory.create(project=project, value=None) us = f.UserStoryFactory.create(project=project, owner=user) @@ -54,7 +54,7 @@ def test_list_milestones_taiga_info_headers(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) role = f.RoleFactory.create(project=project) - f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) f.MilestoneFactory.create(project=project, owner=user, closed=True) f.MilestoneFactory.create(project=project, owner=user, closed=True) diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index c78ab048..85ea89fb 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -712,7 +712,7 @@ def test_resource_notification_test(client, settings, mail): user2 = f.UserFactory.create() project = f.ProjectFactory.create(owner=user1) role = f.RoleFactory.create(project=project, permissions=["view_issues"]) - f.MembershipFactory.create(project=project, user=user1, role=role, is_owner=True) + f.MembershipFactory.create(project=project, user=user1, role=role, is_admin=True) f.MembershipFactory.create(project=project, user=user2, role=role) issue = f.IssueFactory.create(owner=user2, project=project) @@ -750,7 +750,7 @@ def test_watchers_assignation_for_issue(client): project2 = f.ProjectFactory.create(owner=user2) role1 = f.RoleFactory.create(project=project1) role2 = f.RoleFactory.create(project=project2) - f.MembershipFactory.create(project=project1, user=user1, role=role1, is_owner=True) + f.MembershipFactory.create(project=project1, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project2, user=user2, role=role2) client.login(user1) @@ -802,7 +802,7 @@ def test_watchers_assignation_for_task(client): project2 = f.ProjectFactory.create(owner=user2) role1 = f.RoleFactory.create(project=project1, permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) role2 = f.RoleFactory.create(project=project2) - f.MembershipFactory.create(project=project1, user=user1, role=role1, is_owner=True) + f.MembershipFactory.create(project=project1, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project2, user=user2, role=role2) client.login(user1) @@ -854,7 +854,7 @@ def test_watchers_assignation_for_us(client): project2 = f.ProjectFactory.create(owner=user2) role1 = f.RoleFactory.create(project=project1) role2 = f.RoleFactory.create(project=project2) - f.MembershipFactory.create(project=project1, user=user1, role=role1, is_owner=True) + f.MembershipFactory.create(project=project1, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project2, user=user2, role=role2) client.login(user1) diff --git a/tests/integration/test_occ.py b/tests/integration/test_occ.py index b8e22223..580f6733 100644 --- a/tests/integration/test_occ.py +++ b/tests/integration/test_occ.py @@ -30,7 +30,7 @@ pytestmark = pytest.mark.django_db def test_valid_us_creation(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) @@ -47,7 +47,7 @@ def test_valid_us_creation(client): def test_invalid_concurrent_save_for_issue(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save" @@ -76,7 +76,7 @@ def test_invalid_concurrent_save_for_issue(client): def test_valid_concurrent_save_for_issue_different_versions(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save" @@ -105,7 +105,7 @@ def test_valid_concurrent_save_for_issue_different_versions(client): def test_valid_concurrent_save_for_issue_different_fields(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save" @@ -134,7 +134,7 @@ def test_valid_concurrent_save_for_issue_different_fields(client): def test_invalid_concurrent_save_for_wiki_page(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.wiki.api.WikiViewSet.pre_conditions_on_save" @@ -158,7 +158,7 @@ def test_invalid_concurrent_save_for_wiki_page(client): def test_valid_concurrent_save_for_wiki_page_different_versions(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.wiki.api.WikiViewSet.pre_conditions_on_save" @@ -182,7 +182,7 @@ def test_valid_concurrent_save_for_wiki_page_different_versions(client): def test_invalid_concurrent_save_for_us(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) f.UserStoryFactory.create(version=10, project=project) client.login(user) @@ -209,7 +209,7 @@ def test_invalid_concurrent_save_for_us(client): def test_valid_concurrent_save_for_us_different_versions(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.userstories.api.UserStoryViewSet.pre_conditions_on_save" @@ -235,7 +235,7 @@ def test_valid_concurrent_save_for_us_different_versions(client): def test_valid_concurrent_save_for_us_different_fields(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.userstories.api.UserStoryViewSet.pre_conditions_on_save" @@ -261,7 +261,7 @@ def test_valid_concurrent_save_for_us_different_fields(client): def test_invalid_concurrent_save_for_task(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save" @@ -287,7 +287,7 @@ def test_invalid_concurrent_save_for_task(client): def test_valid_concurrent_save_for_task_different_versions(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save" @@ -313,7 +313,7 @@ def test_valid_concurrent_save_for_task_different_versions(client): def test_valid_concurrent_save_for_task_different_fields(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save" @@ -340,7 +340,7 @@ def test_valid_concurrent_save_for_task_different_fields(client): def test_invalid_save_without_version_parameter(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) client.login(user) mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save" diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py index b16bcea3..ddcf9e34 100644 --- a/tests/integration/test_permissions.py +++ b/tests/integration/test_permissions.py @@ -64,11 +64,11 @@ def test_owner_member_get_user_project_permissions(): project.anon_permissions = ["test1"] project.public_permissions = ["test2"] role = factories.RoleFactory(permissions=["test3"]) - factories.MembershipFactory(user=user1, project=project, role=role, is_owner=True) + factories.MembershipFactory(user=user1, project=project, role=role, is_admin=True) expected_perms = set( ["test1", "test2", "test3"] + - [x[0] for x in permissions.OWNERS_PERMISSIONS] + + [x[0] for x in permissions.ADMINS_PERMISSIONS] + [x[0] for x in permissions.MEMBERS_PERMISSIONS] ) assert service.get_user_project_permissions(user1, project) == expected_perms diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index e3c80617..b086f402 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1,6 +1,8 @@ from django.core.urlresolvers import reverse from django.conf import settings from django.core.files import File +from django.core import mail +from django.core import signing from taiga.base.utils import json from taiga.projects.services import stats as stats_services @@ -19,6 +21,17 @@ import pytest pytestmark = pytest.mark.django_db +class ExpiredSigner(signing.TimestampSigner): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.salt = "django.core.signing.TimestampSigner" + + def timestamp(self): + from django.utils import baseconv + import time + time_in_the_far_past = int(time.time()) - 24*60*60*1000 + return baseconv.base62.encode(time_in_the_far_past) + def test_get_project_by_slug(client): project = f.create_project() @@ -43,9 +56,156 @@ def test_create_project(client): assert response.status_code == 201 +def test_create_private_project_without_enough_private_projects_slots(client): + user = f.create_user(max_private_projects=0) + url = reverse("projects-list") + data = { + "name": "project name", + "description": "project description", + "is_private": True + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" + + +def test_create_public_project_without_enough_public_projects_slots(client): + user = f.create_user(max_public_projects=0) + url = reverse("projects-list") + data = { + "name": "project name", + "description": "project description", + "is_private": False + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "can't have more public projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" + + +def test_change_project_from_private_to_public_without_enough_public_projects_slots(client): + project = f.create_project(is_private=True, owner__max_public_projects=0) + f.MembershipFactory(user=project.owner, project=project, is_admin=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + data = { + "is_private": False + } + + client.login(project.owner) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 400 + assert "can't have more public projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "False" + + +def test_change_project_from_public_to_private_without_enough_private_projects_slots(client): + project = f.create_project(is_private=False, owner__max_private_projects=0) + f.MembershipFactory(user=project.owner, project=project, is_admin=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + data = { + "is_private": True + } + + client.login(project.owner) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 400 + assert "can't have more private projects" in response.data["_error_message"] + assert response["Taiga-Info-Project-Memberships"] == "1" + assert response["Taiga-Info-Project-Is-Private"] == "True" + + +def test_create_private_project_with_enough_private_projects_slots(client): + user = f.create_user(max_private_projects=1) + url = reverse("projects-list") + data = { + "name": "project name", + "description": "project description", + "is_private": True + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + +def test_create_public_project_with_enough_public_projects_slots(client): + user = f.create_user(max_public_projects=1) + url = reverse("projects-list") + data = { + "name": "project name", + "description": "project description", + "is_private": False + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + +def test_change_project_from_private_to_public_with_enough_public_projects_slots(client): + project = f.create_project(is_private=True, owner__max_public_projects=1) + f.MembershipFactory(user=project.owner, project=project, is_admin=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + data = { + "is_private": False + } + + client.login(project.owner) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200 + + +def test_change_project_from_public_to_private_with_enough_private_projects_slots(client): + project = f.create_project(is_private=False, owner__max_private_projects=1) + f.MembershipFactory(user=project.owner, project=project, is_admin=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + data = { + "is_private": True + } + + client.login(project.owner) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200 + + +def test_change_project_other_data_with_enough_private_projects_slots(client): + project = f.create_project(is_private=True, owner__max_private_projects=1) + f.MembershipFactory(user=project.owner, project=project, is_admin=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + data = { + "name": "test-project-change" + } + + client.login(project.owner) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200 + + def test_partially_update_project(client): project = f.create_project() - f.MembershipFactory(user=project.owner, project=project, is_owner=True) + f.MembershipFactory(user=project.owner, project=project, is_admin=True) url = reverse("projects-detail", kwargs={"pk": project.pk}) data = {"name": ""} @@ -96,7 +256,7 @@ def test_task_status_is_closed_changed_recalc_us_is_closed(client): def test_us_status_slug_generation(client): us_status = f.UserStoryStatusFactory(name="NEW") - f.MembershipFactory(user=us_status.project.owner, project=us_status.project, is_owner=True) + f.MembershipFactory(user=us_status.project.owner, project=us_status.project, is_admin=True) assert us_status.slug == "new" client.login(us_status.project.owner) @@ -116,7 +276,7 @@ def test_us_status_slug_generation(client): def test_task_status_slug_generation(client): task_status = f.TaskStatusFactory(name="NEW") - f.MembershipFactory(user=task_status.project.owner, project=task_status.project, is_owner=True) + f.MembershipFactory(user=task_status.project.owner, project=task_status.project, is_admin=True) assert task_status.slug == "new" client.login(task_status.project.owner) @@ -136,7 +296,7 @@ def test_task_status_slug_generation(client): def test_issue_status_slug_generation(client): issue_status = f.IssueStatusFactory(name="NEW") - f.MembershipFactory(user=issue_status.project.owner, project=issue_status.project, is_owner=True) + f.MembershipFactory(user=issue_status.project.owner, project=issue_status.project, is_admin=True) assert issue_status.slug == "new" client.login(issue_status.project.owner) @@ -157,7 +317,7 @@ def test_issue_status_slug_generation(client): def test_points_name_duplicated(client): point_1 = f.PointsFactory() point_2 = f.PointsFactory(project=point_1.project) - f.MembershipFactory(user=point_1.project.owner, project=point_1.project, is_owner=True) + f.MembershipFactory(user=point_1.project.owner, project=point_1.project, is_admin=True) client.login(point_1.project.owner) url = reverse("points-detail", kwargs={"pk": point_2.pk}) @@ -244,12 +404,27 @@ def test_leave_project_valid_membership_only_owner(client): user = f.UserFactory.create() project = f.ProjectFactory.create() role = f.RoleFactory.create(project=project, permissions=["view_project"]) - f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) client.login(user) url = reverse("projects-leave", args=(project.id,)) response = client.post(url) assert response.status_code == 403 - assert response.data["_error_message"] == "You can't leave the project if there are no more owners" + assert response.data["_error_message"] == "You can't leave the project if you are the owner or there are no more admins" + + +def test_leave_project_valid_membership_real_owner(client): + owner_user = f.UserFactory.create() + member_user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=owner_user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + f.MembershipFactory.create(project=project, user=owner_user, role=role, is_admin=True) + f.MembershipFactory.create(project=project, user=member_user, role=role, is_admin=True) + + client.login(owner_user) + url = reverse("projects-leave", args=(project.id,)) + response = client.post(url) + assert response.status_code == 403 + assert response.data["_error_message"] == "You can't leave the project if you are the owner or there are no more admins" def test_leave_project_invalid_membership(client): @@ -281,34 +456,49 @@ def test_delete_membership_only_owner(client): user = f.UserFactory.create() project = f.ProjectFactory.create() role = f.RoleFactory.create(project=project, permissions=["view_project"]) - membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) client.login(user) url = reverse("memberships-detail", args=(membership.id,)) response = client.delete(url) assert response.status_code == 400 - assert response.data["_error_message"] == "At least one of the user must be an active admin" + assert response.data["_error_message"] == "The project must have an owner and at least one of the users must be an active admin" + + +def test_delete_membership_real_owner(client): + owner_user = f.UserFactory.create() + member_user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=owner_user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + owner_membership = f.MembershipFactory.create(project=project, user=owner_user, role=role, is_admin=True) + f.MembershipFactory.create(project=project, user=member_user, role=role, is_admin=True) + + client.login(owner_user) + url = reverse("memberships-detail", args=(owner_membership.id,)) + response = client.delete(url) + assert response.status_code == 400 + assert response.data["_error_message"] == "The project must have an owner and at least one of the users must be an active admin" def test_edit_membership_only_owner(client): user = f.UserFactory.create() project = f.ProjectFactory.create() role = f.RoleFactory.create(project=project, permissions=["view_project"]) - membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) data = { - "is_owner": False + "is_admin": False } client.login(user) url = reverse("memberships-detail", args=(membership.id,)) response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 - assert response.data["is_owner"][0] == "At least one of the user must be an active admin" + assert response.data["is_admin"][0] == "At least one user must be an active admin for this project." def test_anon_permissions_generation_when_making_project_public(client): user = f.UserFactory.create() project = f.ProjectFactory.create(is_private=True) role = f.RoleFactory.create(project=project, permissions=["view_project", "modify_project"]) - membership = f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) assert project.anon_permissions == [] client.login(user) url = reverse("projects-detail", kwargs={"pk": project.pk}) @@ -322,7 +512,7 @@ def test_anon_permissions_generation_when_making_project_public(client): def test_destroy_point_and_reassign(client): project = f.ProjectFactory.create() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) p1 = f.PointsFactory(project=project) project.default_points = p1 project.save() @@ -367,7 +557,7 @@ def test_create_and_use_template(client): user = f.UserFactory.create(is_superuser=True) project = f.create_project() role = f.RoleFactory(project=project) - f.MembershipFactory(user=user, project=project, is_owner=True, role=role) + f.MembershipFactory(user=user, project=project, is_admin=True, role=role) client.login(user) url = reverse("projects-create-template", kwargs={"pk": project.pk}) @@ -393,11 +583,11 @@ def test_projects_user_order(client): user = f.UserFactory.create(is_superuser=True) project_1 = f.create_project() role_1 = f.RoleFactory(project=project_1) - f.MembershipFactory(user=user, project=project_1, is_owner=True, role=role_1, user_order=2) + f.MembershipFactory(user=user, project=project_1, is_admin=True, role=role_1, user_order=2) project_2 = f.create_project() role_2 = f.RoleFactory(project=project_2) - f.MembershipFactory(user=user, project=project_2, is_owner=True, role=role_2, user_order=1) + f.MembershipFactory(user=user, project=project_2, is_admin=True, role=role_2, user_order=1) client.login(user) #Testing default id order @@ -547,3 +737,890 @@ def test_project_list_with_search_query_order_by_ranking(client): assert response.data[0]["id"] == project3.id assert response.data[1]["id"] == project2.id assert response.data[2]["id"] == project1.id + + +#################################################################################### +# Test transfer project ownership +#################################################################################### + + +def test_transfer_request_from_not_anonimous(client): + user = f.UserFactory.create() + project = f.create_project(anon_permissions=["view_project"]) + + url = reverse("projects-transfer-request", args=(project.id,)) + + mail.outbox = [] + + response = client.json.post(url) + assert response.status_code == 401 + assert len(mail.outbox) == 0 + + +def test_transfer_request_from_not_project_member(client): + user = f.UserFactory.create() + project = f.create_project(public_permissions=["view_project"]) + + url = reverse("projects-transfer-request", args=(project.id,)) + + mail.outbox = [] + + client.login(user) + response = client.json.post(url) + assert response.status_code == 403 + assert len(mail.outbox) == 0 + + +def test_transfer_request_from_not_admin_member(client): + user = f.UserFactory.create() + project = f.create_project() + role = f.RoleFactory(project=project, permissions=["view_project"]) + f.create_membership(user=user, project=project, role=role, is_admin=False) + + url = reverse("projects-transfer-request", args=(project.id,)) + + mail.outbox = [] + + client.login(user) + response = client.json.post(url) + assert response.status_code == 403 + assert len(mail.outbox) == 0 + + +def test_transfer_request_from_admin_member(client): + user = f.UserFactory.create() + project = f.create_project() + role = f.RoleFactory(project=project, permissions=["view_project"]) + f.create_membership(user=user, project=project, role=role, is_admin=True) + + url = reverse("projects-transfer-request", args=(project.id,)) + + mail.outbox = [] + + client.login(user) + response = client.json.post(url) + assert response.status_code == 200 + assert len(mail.outbox) == 1 + + +def test_project_transfer_start_to_not_a_membership(client): + user_from = f.UserFactory.create() + project = f.create_project(owner=user_from) + f.create_membership(user=user_from, project=project, is_admin=True) + + client.login(user_from) + url = reverse("projects-transfer-start", kwargs={"pk": project.pk}) + + data = { + "user": 666, + } + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert "The user doesn't exist" in response.data + + +def test_project_transfer_start_to_a_not_admin_member(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + project = f.create_project(owner=user_from) + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_from) + url = reverse("projects-transfer-start", kwargs={"pk": project.pk}) + + data = { + "user": user_to.id, + } + mail.outbox = [] + + assert project.transfer_token is None + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.id) + assert project.transfer_token is not None + assert len(mail.outbox) == 1 + + +def test_project_transfer_start_to_an_admin_member(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + project = f.create_project(owner=user_from) + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project, is_admin=True) + + client.login(user_from) + url = reverse("projects-transfer-start", kwargs={"pk": project.pk}) + + data = { + "user": user_to.id, + } + mail.outbox = [] + + assert project.transfer_token is None + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.id) + assert project.transfer_token is not None + assert len(mail.outbox) == 1 + + +def test_project_transfer_reject_from_member_without_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-reject", kwargs={"pk": project.pk}) + + data = {} + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert len(mail.outbox) == 0 + + +def test_project_transfer_reject_from_member_with_invalid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + project = f.create_project(owner=user_from, transfer_token="invalid-token") + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-reject", kwargs={"pk": project.pk}) + + data = { + "token": "invalid-token", + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token is invalid" == response.data["_error_message"] + assert len(mail.outbox) == 0 + + +def test_project_transfer_reject_from_member_with_other_user_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + other_user = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(other_user.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-reject", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token is invalid" == response.data["_error_message"] + assert len(mail.outbox) == 0 + + +def test_project_transfer_reject_from_member_with_expired_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = ExpiredSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-reject", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token has expired" == response.data["_error_message"] + assert len(mail.outbox) == 0 + + +def test_project_transfer_reject_from_admin_member_with_valid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project, is_admin=True) + + client.login(user_to) + url = reverse("projects-transfer-reject", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert len(mail.outbox) == 1 + assert mail.outbox[0].to == [user_from.email] + project = Project.objects.get(pk=project.pk) + assert project.owner.id == user_from.id + assert project.transfer_token is None + + +def test_project_transfer_reject_from_no_admin_member_with_valid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + m = f.create_membership(user=user_to, project=project, is_admin=False) + + client.login(user_to) + url = reverse("projects-transfer-reject", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert len(mail.outbox) == 1 + assert mail.outbox[0].to == [user_from.email] + assert m.is_admin == False + project = Project.objects.get(pk=project.pk) + m = project.memberships.get(user=user_to) + assert project.owner.id == user_from.id + assert project.transfer_token is None + assert m.is_admin == False + + +def test_project_transfer_accept_from_member_without_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = {} + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert len(mail.outbox) == 0 + + +def test_project_transfer_accept_from_member_with_invalid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + project = f.create_project(owner=user_from, transfer_token="invalid-token") + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": "invalid-token", + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token is invalid" == response.data["_error_message"] + assert len(mail.outbox) == 0 + + +def test_project_transfer_accept_from_member_with_other_user_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + other_user = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(other_user.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token is invalid" == response.data["_error_message"] + assert len(mail.outbox) == 0 + + +def test_project_transfer_accept_from_member_with_expired_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = ExpiredSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token has expired" == response.data["_error_message"] + assert len(mail.outbox) == 0 + + +def test_project_transfer_accept_from_member_with_valid_token_without_enough_slots(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_private_projects=0) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=True) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert len(mail.outbox) == 0 + project = Project.objects.get(pk=project.pk) + assert project.owner.id == user_from.id + assert project.transfer_token is not None + + +def test_project_transfer_accept_from_member_with_valid_token_without_enough_memberships_public_project_slots(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_memberships_public_projects=5) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=False) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + f.create_membership(project=project) + f.create_membership(project=project) + f.create_membership(project=project) + f.create_membership(project=project) + f.create_membership(project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert len(mail.outbox) == 0 + project = Project.objects.get(pk=project.pk) + assert project.owner.id == user_from.id + assert project.transfer_token is not None + + +def test_project_transfer_accept_from_member_with_valid_token_without_enough_memberships_private_project_slots(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_memberships_private_projects=5) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=True) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + f.create_membership(project=project) + f.create_membership(project=project) + f.create_membership(project=project) + f.create_membership(project=project) + f.create_membership(project=project) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert len(mail.outbox) == 0 + project = Project.objects.get(pk=project.pk) + assert project.owner.id == user_from.id + assert project.transfer_token is not None + + +def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_slots(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_private_projects=1) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=True) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project, is_admin=True) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert len(mail.outbox) == 1 + assert mail.outbox[0].to == [user_from.email] + project = Project.objects.get(pk=project.pk) + assert project.owner.id == user_to.id + assert project.transfer_token is None + + +def test_project_transfer_accept_from_no_admin_member_with_valid_token_with_enough_slots(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_private_projects=1) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=True) + + f.create_membership(user=user_from, project=project, is_admin=True) + m = f.create_membership(user=user_to, project=project, is_admin=False) + + client.login(user_to) + url = reverse("projects-transfer-accept", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + mail.outbox = [] + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert len(mail.outbox) == 1 + assert mail.outbox[0].to == [user_from.email] + assert m.is_admin == False + project = Project.objects.get(pk=project.pk) + m = project.memberships.get(user=user_to) + assert project.owner.id == user_to.id + assert project.transfer_token is None + assert m.is_admin == True + + +def test_project_transfer_validate_token_from_member_without_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk}) + + data = {} + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_project_transfer_validate_token_from_member_with_invalid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + project = f.create_project(owner=user_from, transfer_token="invalid-token") + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk}) + + data = { + "token": "invalid-token", + } + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token is invalid" == response.data["_error_message"] + + +def test_project_transfer_validate_token_from_member_with_other_user_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + other_user = f.UserFactory.create() + + signer = signing.TimestampSigner() + token = signer.sign(other_user.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token is invalid" == response.data["_error_message"] + + +def test_project_transfer_validate_token_from_member_with_expired_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create() + + signer = ExpiredSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project) + + client.login(user_to) + url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "Token has expired" == response.data["_error_message"] + + + +def test_project_transfer_validate_token_from_admin_member_with_valid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_private_projects=1) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=True) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project, is_admin=True) + + client.login(user_to) + url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + +def test_project_transfer_validate_token_from_no_admin_member_with_valid_token(client): + user_from = f.UserFactory.create() + user_to = f.UserFactory.create(max_private_projects=1) + + signer = signing.TimestampSigner() + token = signer.sign(user_to.id) + project = f.create_project(owner=user_from, transfer_token=token, is_private=True) + + f.create_membership(user=user_from, project=project, is_admin=True) + f.create_membership(user=user_to, project=project, is_admin=False) + + client.login(user_to) + url = reverse("projects-transfer-validate-token", kwargs={"pk": project.pk}) + + data = { + "token": token, + } + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + +#################################################################################### +# Test taiga.projects.services.members.check_if_project_privacity_can_be_changed +#################################################################################### + +from taiga.projects.services.members import ( + check_if_project_privacity_can_be_changed, + ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS, + ERROR_MAX_PUBLIC_PROJECTS, + ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS, + ERROR_MAX_PRIVATE_PROJECTS +) + +# private to public + +def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot_and_too_much_members(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = 0 + project.owner.max_memberships_public_projects = 3 + + assert (check_if_project_privacity_can_be_changed(project) == + {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS}) + + +def test_private_project_cant_be_public_because_owner_doesnt_have_enought_slot(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = 0 + project.owner.max_memberships_public_projects = 6 + + assert (check_if_project_privacity_can_be_changed(project) == + {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS}) + + +def test_private_project_cant_be_public_because_too_much_members(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = 2 + project.owner.max_memberships_public_projects = 3 + + assert (check_if_project_privacity_can_be_changed(project) == + {'can_be_updated': False, 'reason': ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS}) + + +def test_private_project_can_be_public_because_owner_has_enought_slot_and_project_has_enought_members(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = 2 + project.owner.max_memberships_public_projects = 6 + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +def test_private_project_can_be_public_because_owner_has_unlimited_slot_and_project_has_unlimited_members(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = None + project.owner.max_memberships_public_projects = None + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +def test_private_project_can_be_public_because_owner_has_unlimited_slot(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = None + project.owner.max_memberships_public_projects = 6 + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +def test_private_project_can_be_public_because_project_has_unlimited_members(client): + project = f.create_project(is_private=True) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_public_projects = 2 + project.owner.max_memberships_public_projects = None + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +# public to private + +def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot_and_too_much_members(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = 0 + project.owner.max_memberships_private_projects = 3 + + assert (check_if_project_privacity_can_be_changed(project) == + {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS}) + + +def test_public_project_cant_be_private_because_owner_doesnt_have_enought_slot(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = 0 + project.owner.max_memberships_private_projects = 6 + + assert (check_if_project_privacity_can_be_changed(project) == + {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS}) + + +def test_public_project_cant_be_private_because_too_much_members(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = 2 + project.owner.max_memberships_private_projects = 3 + + assert (check_if_project_privacity_can_be_changed(project) == + {'can_be_updated': False, 'reason': ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS}) + + +def test_public_project_can_be_private_because_owner_has_enought_slot_and_project_has_enought_members(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = 2 + project.owner.max_memberships_private_projects = 6 + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +def test_public_project_can_be_private_because_owner_has_unlimited_slot_and_project_has_unlimited_members(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = None + project.owner.max_memberships_private_projects = None + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +def test_public_project_can_be_private_because_owner_has_unlimited_slot(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = None + project.owner.max_memberships_private_projects = 6 + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) + + +def test_public_project_can_be_private_because_project_has_unlimited_members(client): + project = f.create_project(is_private=False) + f.MembershipFactory(project=project, user=project.owner) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + f.MembershipFactory(project=project) + + project.owner.max_private_projects = 2 + project.owner.max_memberships_private_projects = None + + assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None}) diff --git a/tests/integration/test_references_sequences.py b/tests/integration/test_references_sequences.py index f1a96e9a..6c856037 100644 --- a/tests/integration/test_references_sequences.py +++ b/tests/integration/test_references_sequences.py @@ -152,7 +152,7 @@ def test_params_validation_in_api_request(client, refmodels): project = factories.ProjectFactory.create(owner=user) seqname1 = refmodels.make_sequence_name(project) role = factories.RoleFactory.create(project=project) - factories.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + factories.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) milestone = factories.MilestoneFactory.create(project=project) us = factories.UserStoryFactory.create(project=project) diff --git a/tests/integration/test_roles.py b/tests/integration/test_roles.py index 0a39b53e..7dc978f5 100644 --- a/tests/integration/test_roles.py +++ b/tests/integration/test_roles.py @@ -36,7 +36,7 @@ def test_destroy_role_and_reassign_members(client): project = f.ProjectFactory.create(owner=user1) role1 = f.RoleFactory.create(project=project) role2 = f.RoleFactory.create(project=project) - f.MembershipFactory.create(project=project, user=user1, role=role1, is_owner=True) + f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project, user=user2, role=role2) url = reverse("roles-detail", args=[role2.pk]) + "?moveTo={}".format(role1.pk) diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 2fb2fc78..2e8203fd 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -43,7 +43,7 @@ def test_create_task_without_status(client): project.default_task_status = status project.save() - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("tasks-list") data = {"subject": "Test user story", "project": project.id} @@ -56,7 +56,7 @@ def test_create_task_without_status(client): def test_create_task_without_default_values(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user, default_task_status=None) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("tasks-list") data = {"subject": "Test user story", "project": project.id} @@ -69,7 +69,7 @@ def test_create_task_without_default_values(client): def test_api_update_task_tags(client): project = f.ProjectFactory.create() task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) - f.MembershipFactory.create(project=project, user=task.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) url = reverse("tasks-detail", kwargs={"pk": task.pk}) data = {"tags": ["back", "front"], "version": task.version} @@ -81,7 +81,7 @@ def test_api_update_task_tags(client): def test_api_create_in_bulk_with_status(client): us = f.create_userstory() - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) us.project.default_task_status = f.TaskStatusFactory.create(project=us.project) url = reverse("tasks-bulk-create") data = { @@ -104,7 +104,7 @@ def test_api_create_invalid_task(client): # But the User Story is not associated with the milestone us_milestone = f.MilestoneFactory.create() us = f.create_userstory(milestone=us_milestone) - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) us.project.default_task_status = f.TaskStatusFactory.create(project=us.project) task_milestone = f.MilestoneFactory.create(project=us.project, owner=us.owner) @@ -124,7 +124,7 @@ def test_api_create_invalid_task(client): def test_api_update_order_in_bulk(client): project = f.create_project() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) task1 = f.create_task(project=project) task2 = f.create_task(project=project) diff --git a/tests/integration/test_throwttling.py b/tests/integration/test_throwttling.py index d3c049ed..e231188f 100644 --- a/tests/integration/test_throwttling.py +++ b/tests/integration/test_throwttling.py @@ -55,7 +55,7 @@ def test_anonimous_throttling_policy(client, settings): def test_user_throttling_policy(client, settings): project = f.create_project() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) url = reverse("projects-detail", kwargs={"pk": project.pk}) client.login(project.owner) @@ -84,7 +84,7 @@ def test_user_throttling_policy(client, settings): def test_import_mode_throttling_policy(client, settings): project = f.create_project() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) project.default_issue_type = f.IssueTypeFactory.create(project=project) project.default_issue_status = f.IssueStatusFactory.create(project=project) project.default_severity = f.SeverityFactory.create(project=project) diff --git a/tests/integration/test_totals_projects.py b/tests/integration/test_totals_projects.py index 6b46983b..c9bc14b3 100644 --- a/tests/integration/test_totals_projects.py +++ b/tests/integration/test_totals_projects.py @@ -112,7 +112,7 @@ def test_project_totals_updated_on_activity(client): def test_project_totals_updated_on_like(client): project = f.create_project() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) totals_updated_datetime = project.totals_updated_datetime now = datetime.datetime.now() @@ -146,7 +146,6 @@ def test_project_totals_updated_on_like(client): client.login(project.owner) url_like = reverse("projects-like", args=(project.id,)) response = client.post(url_like) - print(response.data) project = Project.objects.get(id=project.id) assert project.total_fans == 4 diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 88b29523..360c6054 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -15,6 +15,7 @@ from taiga.users import models from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer from taiga.auth.tokens import get_token_for_user from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects import choices as project_choices from taiga.users.services import get_watched_list, get_voted_list, get_liked_list from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.models import NotifyPolicy @@ -152,6 +153,18 @@ def test_delete_self_user(client): assert user.full_name == "Deleted user" +def test_delete_self_user_blocking_projects(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + url = reverse('users-detail', kwargs={"pk": user.pk}) + + assert project.blocked_code == None + client.login(user) + response = client.delete(url) + project = user.owned_projects.first() + assert project.blocked_code == project_choices.BLOCKED_BY_OWNER_LEAVING + + def test_cancel_self_user_with_valid_token(client): user = f.UserFactory.create() url = reverse('users-cancel') @@ -214,8 +227,6 @@ def test_change_avatar_with_long_file_name(client): post_data = {'avatar': avatar} response = client.post(url, post_data) - print(response.data) - assert response.status_code == 200 @@ -487,6 +498,7 @@ def test_get_watched_list_valid_info_for_project(): assert project_watch_info["project_name"] == None assert project_watch_info["project_slug"] == None assert project_watch_info["project_is_private"] == None + assert project_watch_info["project_blocked_code"] == None assert project_watch_info["assigned_to_username"] == None assert project_watch_info["assigned_to_full_name"] == None assert project_watch_info["assigned_to_photo"] == None @@ -546,6 +558,7 @@ def test_get_liked_list_valid_info(): assert project_like_info["project_name"] == None assert project_like_info["project_slug"] == None assert project_like_info["project_is_private"] == None + assert project_like_info["project_blocked_code"] == None assert project_like_info["assigned_to_username"] == None assert project_like_info["assigned_to_full_name"] == None assert project_like_info["assigned_to_photo"] == None @@ -599,6 +612,7 @@ def test_get_watched_list_valid_info_for_not_project_types(): assert instance_watch_info["project_name"] == instance.project.name assert instance_watch_info["project_slug"] == instance.project.slug assert instance_watch_info["project_is_private"] == instance.project.is_private + assert instance_watch_info["project_blocked_code"] == instance.project.blocked_code assert instance_watch_info["assigned_to_username"] == instance.assigned_to.username assert instance_watch_info["assigned_to_full_name"] == instance.assigned_to.full_name assert instance_watch_info["assigned_to_photo"] != "" @@ -655,6 +669,7 @@ def test_get_voted_list_valid_info(): assert instance_vote_info["project_name"] == instance.project.name assert instance_vote_info["project_slug"] == instance.project.slug assert instance_vote_info["project_is_private"] == instance.project.is_private + assert instance_vote_info["project_blocked_code"] == instance.project.blocked_code assert instance_vote_info["assigned_to_username"] == instance.assigned_to.username assert instance_vote_info["assigned_to_full_name"] == instance.assigned_to.full_name assert instance_vote_info["assigned_to_photo"] != "" diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index c50a9710..75b3098a 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -50,8 +50,8 @@ def test_create_userstory_with_watchers(client): user = f.UserFactory.create() user_watcher = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) - f.MembershipFactory.create(project=project, user=user_watcher, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + f.MembershipFactory.create(project=project, user=user_watcher, is_admin=True) url = reverse("userstories-list") data = {"subject": "Test user story", "project": project.id, "watchers": [user_watcher.id]} @@ -69,7 +69,7 @@ def test_create_userstory_without_status(client): project.default_us_status = status project.save() - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("userstories-list") data = {"subject": "Test user story", "project": project.id} @@ -82,7 +82,7 @@ def test_create_userstory_without_status(client): def test_create_userstory_without_default_values(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user, default_us_status=None) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("userstories-list") data = {"subject": "Test user story", "project": project.id} @@ -94,7 +94,7 @@ def test_create_userstory_without_default_values(client): def test_api_delete_userstory(client): us = f.UserStoryFactory.create() - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) url = reverse("userstories-detail", kwargs={"pk": us.pk}) client.login(us.owner) @@ -106,7 +106,7 @@ def test_api_delete_userstory(client): def test_api_filter_by_subject_or_ref(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) f.UserStoryFactory.create(project=project) f.UserStoryFactory.create(project=project, subject="some random subject") @@ -122,7 +122,7 @@ def test_api_filter_by_subject_or_ref(client): def test_api_create_in_bulk_with_status(client): project = f.create_project() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) url = reverse("userstories-bulk-create") data = { "bulk_stories": "Story #1\nStory #2", @@ -139,7 +139,7 @@ def test_api_create_in_bulk_with_status(client): def test_api_update_orders_in_bulk(client): project = f.create_project() - f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) us1 = f.create_userstory(project=project) us2 = f.create_userstory(project=project) @@ -172,7 +172,7 @@ def test_update_userstory_points(client): role1 = f.RoleFactory.create(project=project) role2 = f.RoleFactory.create(project=project) - f.MembershipFactory.create(project=project, user=user1, role=role1, is_owner=True) + f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project, user=user2, role=role2) f.PointsFactory.create(project=project, value=None) @@ -236,7 +236,7 @@ def test_update_userstory_rolepoints_on_add_new_role(client): def test_archived_filter(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) f.UserStoryFactory.create(project=project) archived_status = f.UserStoryStatusFactory.create(is_archived=True) f.UserStoryFactory.create(status=archived_status, project=project) @@ -261,7 +261,7 @@ def test_archived_filter(client): def test_filter_by_multiple_status(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) f.UserStoryFactory.create(project=project) us1 = f.UserStoryFactory.create(project=project) us2 = f.UserStoryFactory.create(project=project) @@ -479,7 +479,7 @@ def test_update_userstory_respecting_watchers(client): project = f.ProjectFactory.create() us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project) us.add_watcher(watching_user) - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) f.MembershipFactory.create(project=us.project, user=watching_user) client.login(user=us.owner) @@ -496,7 +496,7 @@ def test_update_userstory_update_watchers(client): watching_user = f.create_user() project = f.ProjectFactory.create() us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project) - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) f.MembershipFactory.create(project=us.project, user=watching_user) client.login(user=us.owner) @@ -515,7 +515,7 @@ def test_update_userstory_remove_watchers(client): project = f.ProjectFactory.create() us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project) us.add_watcher(watching_user) - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) f.MembershipFactory.create(project=us.project, user=watching_user) client.login(user=us.owner) @@ -532,7 +532,7 @@ def test_update_userstory_remove_watchers(client): def test_update_userstory_update_tribe_gig(client): project = f.ProjectFactory.create() us = f.UserStoryFactory.create(project=project, status__project=project, milestone__project=project) - f.MembershipFactory.create(project=us.project, user=us.owner, is_owner=True) + f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) url = reverse("userstories-detail", kwargs={"pk": us.pk}) data = { diff --git a/tests/integration/test_vote_issues.py b/tests/integration/test_vote_issues.py index 4af983ae..b6f8e925 100644 --- a/tests/integration/test_vote_issues.py +++ b/tests/integration/test_vote_issues.py @@ -27,7 +27,7 @@ pytestmark = pytest.mark.django_db def test_upvote_issue(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url = reverse("issues-upvote", args=(issue.id,)) client.login(user) @@ -39,7 +39,7 @@ def test_upvote_issue(client): def test_downvote_issue(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url = reverse("issues-downvote", args=(issue.id,)) client.login(user) @@ -51,7 +51,7 @@ def test_downvote_issue(client): def test_list_issue_voters(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) f.VoteFactory.create(content_object=issue, user=user) url = reverse("issue-voters-list", args=(issue.id,)) @@ -64,7 +64,7 @@ def test_list_issue_voters(client): def test_get_issue_voter(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) vote = f.VoteFactory.create(content_object=issue, user=user) url = reverse("issue-voters-detail", args=(issue.id, vote.user.id)) @@ -77,7 +77,7 @@ def test_get_issue_voter(client): def test_get_issue_votes(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url = reverse("issues-detail", args=(issue.id,)) f.VotesFactory.create(content_object=issue, count=5) @@ -92,7 +92,7 @@ def test_get_issue_votes(client): def test_get_issue_is_voted(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) f.VotesFactory.create(content_object=issue) url_detail = reverse("issues-detail", args=(issue.id,)) url_upvote = reverse("issues-upvote", args=(issue.id,)) diff --git a/tests/integration/test_vote_tasks.py b/tests/integration/test_vote_tasks.py index f387474d..ca3414e6 100644 --- a/tests/integration/test_vote_tasks.py +++ b/tests/integration/test_vote_tasks.py @@ -26,8 +26,8 @@ pytestmark = pytest.mark.django_db def test_upvote_task(client): user = f.UserFactory.create() - task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + task = f.create_task(owner=user, milestone=None) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url = reverse("tasks-upvote", args=(task.id,)) client.login(user) @@ -38,8 +38,8 @@ def test_upvote_task(client): def test_downvote_task(client): user = f.UserFactory.create() - task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + task = f.create_task(owner=user, milestone=None) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url = reverse("tasks-downvote", args=(task.id,)) client.login(user) @@ -51,7 +51,7 @@ def test_downvote_task(client): def test_list_task_voters(client): user = f.UserFactory.create() task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) f.VoteFactory.create(content_object=task, user=user) url = reverse("task-voters-list", args=(task.id,)) @@ -65,7 +65,7 @@ def test_list_task_voters(client): def test_get_task_voter(client): user = f.UserFactory.create() task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) vote = f.VoteFactory.create(content_object=task, user=user) url = reverse("task-voters-detail", args=(task.id, vote.user.id)) @@ -79,7 +79,7 @@ def test_get_task_voter(client): def test_get_task_votes(client): user = f.UserFactory.create() task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url = reverse("tasks-detail", args=(task.id,)) f.VotesFactory.create(content_object=task, count=5) @@ -93,8 +93,8 @@ def test_get_task_votes(client): def test_get_task_is_voted(client): user = f.UserFactory.create() - task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + task = f.create_task(owner=user, milestone=None) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) f.VotesFactory.create(content_object=task) url_detail = reverse("tasks-detail", args=(task.id,)) url_upvote = reverse("tasks-upvote", args=(task.id,)) diff --git a/tests/integration/test_vote_userstories.py b/tests/integration/test_vote_userstories.py index 772937b8..b8caa01b 100644 --- a/tests/integration/test_vote_userstories.py +++ b/tests/integration/test_vote_userstories.py @@ -26,8 +26,8 @@ pytestmark = pytest.mark.django_db def test_upvote_user_story(client): user = f.UserFactory.create() - user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url = reverse("userstories-upvote", args=(user_story.id,)) client.login(user) @@ -38,8 +38,8 @@ def test_upvote_user_story(client): def test_downvote_user_story(client): user = f.UserFactory.create() - user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url = reverse("userstories-downvote", args=(user_story.id,)) client.login(user) @@ -51,7 +51,7 @@ def test_downvote_user_story(client): def test_list_user_story_voters(client): user = f.UserFactory.create() user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) f.VoteFactory.create(content_object=user_story, user=user) url = reverse("userstory-voters-list", args=(user_story.id,)) @@ -64,7 +64,7 @@ def test_list_user_story_voters(client): def test_get_userstory_voter(client): user = f.UserFactory.create() user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) vote = f.VoteFactory.create(content_object=user_story, user=user) url = reverse("userstory-voters-detail", args=(user_story.id, vote.user.id)) @@ -78,7 +78,7 @@ def test_get_userstory_voter(client): def test_get_user_story_votes(client): user = f.UserFactory.create() user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url = reverse("userstories-detail", args=(user_story.id,)) f.VotesFactory.create(content_object=user_story, count=5) @@ -92,8 +92,8 @@ def test_get_user_story_votes(client): def test_get_user_story_is_voted(client): user = f.UserFactory.create() - user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) f.VotesFactory.create(content_object=user_story) url_detail = reverse("userstories-detail", args=(user_story.id,)) url_upvote = reverse("userstories-upvote", args=(user_story.id,)) diff --git a/tests/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py index c5010827..fc22f32c 100644 --- a/tests/integration/test_watch_issues.py +++ b/tests/integration/test_watch_issues.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db def test_watch_issue(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url = reverse("issues-watch", args=(issue.id,)) client.login(user) @@ -40,7 +40,7 @@ def test_watch_issue(client): def test_unwatch_issue(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url = reverse("issues-watch", args=(issue.id,)) client.login(user) @@ -52,7 +52,7 @@ def test_unwatch_issue(client): def test_list_issue_watchers(client): user = f.UserFactory.create() issue = f.IssueFactory(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) f.WatchedFactory.create(content_object=issue, user=user) url = reverse("issue-watchers-list", args=(issue.id,)) @@ -66,7 +66,7 @@ def test_list_issue_watchers(client): def test_get_issue_watcher(client): user = f.UserFactory.create() issue = f.IssueFactory(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) watch = f.WatchedFactory.create(content_object=issue, user=user) url = reverse("issue-watchers-detail", args=(issue.id, watch.user.id)) @@ -79,8 +79,8 @@ def test_get_issue_watcher(client): def test_get_issue_watchers(client): user = f.UserFactory.create() - issue = f.IssueFactory(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + issue = f.create_issue(owner=user) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url = reverse("issues-detail", args=(issue.id,)) f.WatchedFactory.create(content_object=issue, user=user) @@ -95,8 +95,8 @@ def test_get_issue_watchers(client): def test_get_issue_is_watcher(client): user = f.UserFactory.create() - issue = f.IssueFactory(owner=user) - f.MembershipFactory.create(project=issue.project, user=user, is_owner=True) + issue = f.create_issue(owner=user) + f.MembershipFactory.create(project=issue.project, user=user, is_admin=True) url_detail = reverse("issues-detail", args=(issue.id,)) url_watch = reverse("issues-watch", args=(issue.id,)) url_unwatch = reverse("issues-unwatch", args=(issue.id,)) diff --git a/tests/integration/test_watch_milestones.py b/tests/integration/test_watch_milestones.py index dcb21524..da17f408 100644 --- a/tests/integration/test_watch_milestones.py +++ b/tests/integration/test_watch_milestones.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db def test_watch_milestone(client): user = f.UserFactory.create() milestone = f.MilestoneFactory(owner=user) - f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True) + f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True) url = reverse("milestones-watch", args=(milestone.id,)) client.login(user) @@ -40,7 +40,7 @@ def test_watch_milestone(client): def test_unwatch_milestone(client): user = f.UserFactory.create() milestone = f.MilestoneFactory(owner=user) - f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True) + f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True) url = reverse("milestones-watch", args=(milestone.id,)) client.login(user) @@ -52,7 +52,7 @@ def test_unwatch_milestone(client): def test_list_milestone_watchers(client): user = f.UserFactory.create() milestone = f.MilestoneFactory(owner=user) - f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True) + f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True) f.WatchedFactory.create(content_object=milestone, user=user) url = reverse("milestone-watchers-list", args=(milestone.id,)) @@ -66,7 +66,7 @@ def test_list_milestone_watchers(client): def test_get_milestone_watcher(client): user = f.UserFactory.create() milestone = f.MilestoneFactory(owner=user) - f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True) + f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True) watch = f.WatchedFactory.create(content_object=milestone, user=user) url = reverse("milestone-watchers-detail", args=(milestone.id, watch.user.id)) @@ -80,7 +80,7 @@ def test_get_milestone_watcher(client): def test_get_milestone_watchers(client): user = f.UserFactory.create() milestone = f.MilestoneFactory(owner=user) - f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True) + f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True) url = reverse("milestones-detail", args=(milestone.id,)) f.WatchedFactory.create(content_object=milestone, user=user) @@ -95,7 +95,7 @@ def test_get_milestone_watchers(client): def test_get_milestone_is_watcher(client): user = f.UserFactory.create() milestone = f.MilestoneFactory(owner=user) - f.MembershipFactory.create(project=milestone.project, user=user, is_owner=True) + f.MembershipFactory.create(project=milestone.project, user=user, is_admin=True) url_detail = reverse("milestones-detail", args=(milestone.id,)) url_watch = reverse("milestones-watch", args=(milestone.id,)) url_unwatch = reverse("milestones-unwatch", args=(milestone.id,)) diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py index f49d9160..2608864b 100644 --- a/tests/integration/test_watch_projects.py +++ b/tests/integration/test_watch_projects.py @@ -30,7 +30,7 @@ pytestmark = pytest.mark.django_db def test_watch_project(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-watch", args=(project.id,)) client.login(user) @@ -42,7 +42,7 @@ def test_watch_project(client): def test_watch_project_with_valid_notify_level(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-watch", args=(project.id,)) client.login(user) @@ -57,7 +57,7 @@ def test_watch_project_with_valid_notify_level(client): def test_watch_project_with_invalid_notify_level(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-watch", args=(project.id,)) client.login(user) @@ -73,7 +73,7 @@ def test_watch_project_with_invalid_notify_level(client): def test_unwatch_project(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-unwatch", args=(project.id,)) client.login(user) @@ -85,7 +85,7 @@ def test_unwatch_project(client): def test_list_project_watchers(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) f.WatchedFactory.create(content_object=project, user=user) url = reverse("project-watchers-list", args=(project.id,)) @@ -99,7 +99,7 @@ def test_list_project_watchers(client): def test_get_project_watcher(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) watch = f.WatchedFactory.create(content_object=project, user=user) url = reverse("project-watchers-detail", args=(project.id, watch.user.id)) @@ -113,7 +113,7 @@ def test_get_project_watcher(client): def test_get_project_watchers(client): user = f.UserFactory.create() project = f.create_project(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user, is_admin=True) url = reverse("projects-detail", args=(project.id,)) f.WatchedFactory.create(content_object=project, user=user) diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py index cde26ed0..38ddd40b 100644 --- a/tests/integration/test_watch_tasks.py +++ b/tests/integration/test_watch_tasks.py @@ -27,8 +27,8 @@ pytestmark = pytest.mark.django_db def test_watch_task(client): user = f.UserFactory.create() - task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + task = f.create_task(owner=user, milestone=None) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url = reverse("tasks-watch", args=(task.id,)) client.login(user) @@ -39,8 +39,8 @@ def test_watch_task(client): def test_unwatch_task(client): user = f.UserFactory.create() - task = f.create_task(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + task = f.create_task(owner=user, milestone=None) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url = reverse("tasks-watch", args=(task.id,)) client.login(user) @@ -52,7 +52,7 @@ def test_unwatch_task(client): def test_list_task_watchers(client): user = f.UserFactory.create() task = f.TaskFactory(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) f.WatchedFactory.create(content_object=task, user=user) url = reverse("task-watchers-list", args=(task.id,)) @@ -66,7 +66,7 @@ def test_list_task_watchers(client): def test_get_task_watcher(client): user = f.UserFactory.create() task = f.TaskFactory(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) watch = f.WatchedFactory.create(content_object=task, user=user) url = reverse("task-watchers-detail", args=(task.id, watch.user.id)) @@ -80,7 +80,7 @@ def test_get_task_watcher(client): def test_get_task_watchers(client): user = f.UserFactory.create() task = f.TaskFactory(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url = reverse("tasks-detail", args=(task.id,)) f.WatchedFactory.create(content_object=task, user=user) @@ -95,8 +95,8 @@ def test_get_task_watchers(client): def test_get_task_is_watcher(client): user = f.UserFactory.create() - task = f.TaskFactory(owner=user) - f.MembershipFactory.create(project=task.project, user=user, is_owner=True) + task = f.create_task(owner=user, milestone=None) + f.MembershipFactory.create(project=task.project, user=user, is_admin=True) url_detail = reverse("tasks-detail", args=(task.id,)) url_watch = reverse("tasks-watch", args=(task.id,)) url_unwatch = reverse("tasks-unwatch", args=(task.id,)) diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py index e17081cd..66ae4e0c 100644 --- a/tests/integration/test_watch_userstories.py +++ b/tests/integration/test_watch_userstories.py @@ -27,8 +27,8 @@ pytestmark = pytest.mark.django_db def test_watch_user_story(client): user = f.UserFactory.create() - user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url = reverse("userstories-watch", args=(user_story.id,)) client.login(user) @@ -39,8 +39,8 @@ def test_watch_user_story(client): def test_unwatch_user_story(client): user = f.UserFactory.create() - user_story = f.create_userstory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url = reverse("userstories-unwatch", args=(user_story.id,)) client.login(user) @@ -52,7 +52,7 @@ def test_unwatch_user_story(client): def test_list_user_story_watchers(client): user = f.UserFactory.create() user_story = f.UserStoryFactory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) f.WatchedFactory.create(content_object=user_story, user=user) url = reverse("userstory-watchers-list", args=(user_story.id,)) @@ -65,8 +65,8 @@ def test_list_user_story_watchers(client): def test_get_user_story_watcher(client): user = f.UserFactory.create() - user_story = f.UserStoryFactory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) watch = f.WatchedFactory.create(content_object=user_story, user=user) url = reverse("userstory-watchers-detail", args=(user_story.id, watch.user.id)) @@ -79,8 +79,8 @@ def test_get_user_story_watcher(client): def test_get_user_story_watchers(client): user = f.UserFactory.create() - user_story = f.UserStoryFactory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url = reverse("userstories-detail", args=(user_story.id,)) f.WatchedFactory.create(content_object=user_story, user=user) @@ -95,8 +95,8 @@ def test_get_user_story_watchers(client): def test_get_user_story_is_watcher(client): user = f.UserFactory.create() - user_story = f.UserStoryFactory(owner=user) - f.MembershipFactory.create(project=user_story.project, user=user, is_owner=True) + user_story = f.create_userstory(owner=user, status=None) + f.MembershipFactory.create(project=user_story.project, user=user, is_admin=True) url_detail = reverse("userstories-detail", args=(user_story.id,)) url_watch = reverse("userstories-watch", args=(user_story.id,)) url_unwatch = reverse("userstories-unwatch", args=(user_story.id,)) diff --git a/tests/integration/test_watch_wikipages.py b/tests/integration/test_watch_wikipages.py index 510c7015..6940368d 100644 --- a/tests/integration/test_watch_wikipages.py +++ b/tests/integration/test_watch_wikipages.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db def test_watch_wikipage(client): user = f.UserFactory.create() wikipage = f.WikiPageFactory(owner=user) - f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True) + f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True) url = reverse("wiki-watch", args=(wikipage.id,)) client.login(user) @@ -40,7 +40,7 @@ def test_watch_wikipage(client): def test_unwatch_wikipage(client): user = f.UserFactory.create() wikipage = f.WikiPageFactory(owner=user) - f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True) + f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True) url = reverse("wiki-watch", args=(wikipage.id,)) client.login(user) @@ -52,7 +52,7 @@ def test_unwatch_wikipage(client): def test_list_wikipage_watchers(client): user = f.UserFactory.create() wikipage = f.WikiPageFactory(owner=user) - f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True) + f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True) f.WatchedFactory.create(content_object=wikipage, user=user) url = reverse("wiki-watchers-list", args=(wikipage.id,)) @@ -66,7 +66,7 @@ def test_list_wikipage_watchers(client): def test_get_wikipage_watcher(client): user = f.UserFactory.create() wikipage = f.WikiPageFactory(owner=user) - f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True) + f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True) watch = f.WatchedFactory.create(content_object=wikipage, user=user) url = reverse("wiki-watchers-detail", args=(wikipage.id, watch.user.id)) @@ -80,7 +80,7 @@ def test_get_wikipage_watcher(client): def test_get_wikipage_watchers(client): user = f.UserFactory.create() wikipage = f.WikiPageFactory(owner=user) - f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True) + f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True) url = reverse("wiki-detail", args=(wikipage.id,)) f.WatchedFactory.create(content_object=wikipage, user=user) @@ -95,7 +95,7 @@ def test_get_wikipage_watchers(client): def test_get_wikipage_is_watcher(client): user = f.UserFactory.create() wikipage = f.WikiPageFactory(owner=user) - f.MembershipFactory.create(project=wikipage.project, user=user, is_owner=True) + f.MembershipFactory.create(project=wikipage.project, user=user, is_admin=True) url_detail = reverse("wiki-detail", args=(wikipage.id,)) url_watch = reverse("wiki-watch", args=(wikipage.id,)) url_unwatch = reverse("wiki-unwatch", args=(wikipage.id,)) diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index e9f94e0c..3e656caa 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -10,35 +10,36 @@ pytestmark = pytest.mark.django_db(transaction=True) import factory -class TestingProjectModel(models.Model): +class AuxProjectModel(models.Model): pass -class TestingModelWithNameAttribute(models.Model): +class AuxModelWithNameAttribute(models.Model): name = models.CharField(max_length=255, null=False, blank=False) - project = models.ForeignKey(TestingProjectModel, null=False, blank=False) + project = models.ForeignKey(AuxProjectModel, null=False, blank=False) -class TestingSerializer(ValidateDuplicatedNameInProjectMixin): +class AuxSerializer(ValidateDuplicatedNameInProjectMixin): class Meta: - model = TestingModelWithNameAttribute + model = AuxModelWithNameAttribute + def test_duplicated_name_validation(): - project = TestingProjectModel.objects.create() - instance_1 = TestingModelWithNameAttribute.objects.create(name="1", project=project) - instance_2 = TestingModelWithNameAttribute.objects.create(name="2", project=project) + project = AuxProjectModel.objects.create() + instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project) + instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project) # No duplicated_name - serializer = TestingSerializer(data={"name": "3", "project": project.id}) + serializer = AuxSerializer(data={"name": "3", "project": project.id}) assert serializer.is_valid() # Create duplicated_name - serializer = TestingSerializer(data={"name": "1", "project": project.id}) + serializer = AuxSerializer(data={"name": "1", "project": project.id}) assert not serializer.is_valid() # Update name to existing one - serializer = TestingSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) + serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) assert not serializer.is_valid() diff --git a/tests/unit/test_slug.py b/tests/unit/test_slug.py index 0bebf51f..9cb4ef5f 100644 --- a/tests/unit/test_slug.py +++ b/tests/unit/test_slug.py @@ -16,9 +16,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.projects.models import Project -from taiga.users.models import User +from django.contrib.auth import get_user_model +from taiga.projects.models import Project from taiga.base.utils.slug import slugify import pytest @@ -38,7 +38,7 @@ def test_slugify_3(): def test_project_slug_with_special_chars(): - user = User.objects.create(username="test") + user = get_user_model().objects.create(username="test") project = Project.objects.create(name="漢字", description="漢字", owner=user) project.save() @@ -46,7 +46,7 @@ def test_project_slug_with_special_chars(): def test_project_with_existing_name_slug_with_special_chars(): - user = User.objects.create(username="test") + user = get_user_model().objects.create(username="test") Project.objects.create(name="漢字", description="漢字", owner=user) project = Project.objects.create(name="漢字", description="漢字", owner=user) diff --git a/tests/unit/test_timeline.py b/tests/unit/test_timeline.py index afebbc09..e34906d6 100644 --- a/tests/unit/test_timeline.py +++ b/tests/unit/test_timeline.py @@ -18,19 +18,18 @@ from unittest.mock import patch, call -from django.core.exceptions import ValidationError +from django.contrib.auth import get_user_model from taiga.timeline import service from taiga.timeline.models import Timeline from taiga.projects.models import Project -from taiga.users.models import User import pytest def test_push_to_timeline_many_objects(): with patch("taiga.timeline.service._add_to_object_timeline") as mock: - users = [User(), User(), User()] + users = [get_user_model(), get_user_model(), get_user_model()] project = Project() service.push_to_timeline(users, project, "test", project.created_date) assert mock.call_count == 3 @@ -45,7 +44,7 @@ def test_push_to_timeline_many_objects(): def test_add_to_objects_timeline(): with patch("taiga.timeline.service._add_to_object_timeline") as mock: - users = [User(), User(), User()] + users = [get_user_model(), get_user_model(), get_user_model()] project = Project() service._add_to_objects_timeline(users, project, "test", project.created_date) assert mock.call_count == 3