diff --git a/.travis.yml b/.travis.yml index d0bb13ca..4a94cfaa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ sudo: false language: python python: - "3.4" + - "3.5" services: - rabbitmq # will start rabbitmq-server cache: diff --git a/AUTHORS.rst b/AUTHORS.rst index 896b7876..7242d6b1 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -7,21 +7,23 @@ The PRIMARY AUTHORS are: - Xavi Julian - Anler Hernández -Special thanks to Kaleidos Open Source S.L. for provice time for taiga +Special thanks to Kaleidos Open Source S.L. for provice time for Taiga development. And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- people who have submitted patches, reported bugs, added translations, helped answer newbie questions, and generally made taiga that much better: -- Andrés Moya -- Yamila Moreno -- Ricky Posner -- Alonso Torres - Alejandro Gómez +- Alonso Torres - Andrea Stagi -- Hector Colina -- Julien Palard -- Joe Letts +- Andrés Moya +- Andrey Alekseenko - Chris Wilson - +- David Burke +- Hector Colina +- Joe Letts +- Julien Palard +- Ricky Posner +- Yamila Moreno +- Brett Profitt diff --git a/CHANGELOG.md b/CHANGELOG.md index abbd0695..396bd18f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,44 @@ # Changelog # +## 1.9.0 Abies Siberica (2015-11-XX) + +### Features + +- Project can be starred or unstarred and the fans list can be obtained. +- US, tasks and Issues can be upvoted or downvoted and the voters list can be obtained. +- Now users can watch public issues, tasks and user stories. +- Add endpoints to show the watchers list for issues, tasks and user stories. +- Add a "field type" property for custom fields: 'text', 'multiline text' and 'date' right now (thanks to [@artlepool](https://github.com/artlepool)). +- Allow multiple actions in the commit messages. +- Now every user that coments USs, Issues or Tasks will be involved in it (add author to the watchers list). +- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved. +- Add custom videoconference system. +- Fix the compatibility with BitBucket webhooks and add issues and issues comments integration. +- Add support for comments in the Gitlab webhooks integration. +- Add externall apps: now Taiga can integrate with hundreds of applications and service. +- Improve searching system, now full text searchs are supported +- Add sha1 hash to attachments to verify the integrity of files (thanks to [@astagi](https://github.com/astagi)). +- i18n. + - Add italian (it) translation. + - Add polish (pl) translation. + - Add portuguese (Brazil) (pt_BR) translation. + - Add russian (ru) translation. + +### Misc +- Made compatible with python 3.5. +- Migrated to django 1.8. +- Update the rest of requirements to the last version. +- Improve export system, now is more efficient and prevents possible crashes with heavy projects. +- API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer. +- API: Add stats/system resource with global server stats (total project, total users....) +- API: Improve and fix some errors in issues/filters_data and userstories/filters_data. +- API: resolver suport ref GET param and return a story, task or issue. +- Webhooks: Add deleted datetime to webhooks responses when isues, tasks or USs are deleted. +- Add headers to allow threading for notification emails about changes to issues, tasks, user stories, and wiki pages. (thanks to [@brett](https://github.com/brettp)). +- Lots of small and not so small bugfixes. + + ## 1.8.0 Saracenia Purpurea (2015-06-18) ### Features diff --git a/README.md b/README.md index 7969f798..5aa5093e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,80 @@ [![Managed with Taiga.io](https://taiga.io/media/support/attachments/article-22/banner-gh.png)](https://taiga.io "Managed with Taiga.io") [![Build Status](https://travis-ci.org/taigaio/taiga-back.svg?branch=master)](https://travis-ci.org/taigaio/taiga-back "Build Status") [![Coverage Status](https://coveralls.io/repos/taigaio/taiga-back/badge.svg?branch=master)](https://coveralls.io/r/taigaio/taiga-back?branch=master "Coverage Status") +[![Dependency Status](https://www.versioneye.com/user/projects/561bd091a193340f32001464/badge.svg?style=flat)](https://www.versioneye.com/user/projects/561bd091a193340f32001464) + + +## Contribute to Taiga ## + +#### Where to start #### + +There are many different ways to contribute to Taiga's development, just find the one that best fits with your skills. Examples of contributions we would love to receive include: + +- **Code patches** +- **Documentation improvements** +- **Translations** +- **Bug reports** +- **Patch reviews** +- **UI enhancements** + +Big features are also welcome but if you want to see your contributions included in Taiga codebase we strongly recommend you start by initiating a chat though our [mailing list](http://groups.google.co.uk/d/forum/taigaio) + + +#### License #### + +Every code patch accepted in taiga codebase is licensed under [AGPL v3.0](http://www.gnu.org/licenses/agpl-3.0.html). You should must be careful to not include any code that can not be licensed under this license. + +Please read carefully [our license](https://github.com/taigaio/taiga-back/blob/master/LICENSE) and ask us if you have any questions. + + +#### Bug reports, enhancements and support #### + +If you **nedd help to setup Taiga**, you want to **talk about some cool enhancemnt** or you have **some questions** please write us to our [mailing list](http://groups.google.com/d/forum/taigaio). + +If you **find a bug** in Taiga you can always report it: + +- in our [mailing list](http://groups.google.com/d/forum/taigaio). +- in [github issues](https://github.com/taigaio/taiga-back/issues). +- send us a mail to support@taiga.io if is a bug related to tree.taiga.io. +- send a mail to security@taiga.io if is a **security bug**. + +One of our fellow Taiga developers will search, find and hunt it as soon as possible. + +Please, before reporting an bug write down how can we reproduce it, your operating system, your browser and version, and if it's possible, a screenshot. Sometimes it take less time to fix a bug if the developer know how to find it and we will solve your problem as fast as possible. + + +#### Documentation improvements #### + +We are gathering lots of information from our users to build and enhance our documentation. If you are the documentation to install or develop with Taiga and find any mistakes, omissions or confused sequences, it is enormously helpful to report it. Or better still, if you believe you can author additions, please make a pull-request to taiga project. + +Currently, we have authored three main documentation hubs: + +- **[API Docs](https://github.com/taigaio/taiga-doc)**: Our API documentation and reference for developing from Taiga API. +- **[Installation Guide](https://github.com/taigaio/taiga-doc)**: If you need to install Taiga on your own server, this is the place to find some guides. +- **[Taiga Support](https://github.com/taigaio/taiga-doc)**: This page is intended to be the support reference page for the users. If you find any mistake, please report it. + + +#### Translation #### + +We are ready now to accept your help translating Taiga. It's easy (and fun!) just access to our team of translators with the link below, set up an account in Transifex and start contributing. Join us to make sure your language is covered! **[Help Taiga to trasnlate content](https://www.transifex.com/signup/ "Help Taiga to trasnlatecontent")** + + +#### Code patches #### + +Taiga will always be glad to receive code patches to update, fix or improve its code. + +If you know how to improve our code base or you found a bug, a security vulnerabilities a performance issue and you think you can solve, we will be very happy to accept your pull-request. If your code requires considerable changes, we recommend you first talk to us directly. We will find the best way to help. + + +#### UI enhancements #### + +Taiga is made for developers and designers. We care enormously about UI because usability and design are both critical aspects of the Taiga experience. + +There are two possible ways to contribute to our UI: +- **Bugs**: If you find a bug regarding front-end, please report it as previously indicated in the Bug reports section or send a pull-request as indicated in the Code Patches section. +- **Enhancements**: If its a design or UX bug or enhancement we will love to receive your feedback. Please send us your enhancement, with the reason and, if it's possible, an example. Our design and UX team will review your enhancement and fix it as soon as possible. We recommend you to use our [mailing list](http://groups.google.co.uk/d/forum/taigaio){target="_blank"} so we can have a lot of different opinions and debate. +- **Language Localization**: We are eager to offer localized versions of Taiga. Some members of the community have already volunteered to work to provide a variety of languages. We are working to implement some changes to allow for this and expect to accept these requests in the near future. + ## Setup development environment ## @@ -24,15 +98,3 @@ Initial auth data: admin/123123 If you want a complete environment for production usage, you can try the taiga bootstrapping scripts https://github.com/taigaio/taiga-scripts (warning: alpha state). All the information about the different installation methods (production, development, vagrant, docker...) can be found here http://taigaio.github.io/taiga-doc/dist/#_installation_guide. - -## Community ## - -[Taiga has a mailing list](http://groups.google.com/d/forum/taigaio). Feel free to join it and ask any questions you may have. - -To subscribe for announcements of releases, important changes and so on, please follow [@taigaio](https://twitter.com/taigaio) on Twitter. - -## Donations ## - -We are grateful for your emails volunteering donations to Taiga. We feel comfortable accepting them under these conditions: The first that we will only do so while we are in the current beta / pre-revenue stage and that whatever money is donated will go towards a bounty fund. Starting Q2 2015 we will be engaging much more actively with our community to help further the development of Taiga, and we will use these donations to reward people working alongside us. - -If you wish to make a donation to this Taiga fund, you can do so via http://www.paypal.com using the email: eposner@taiga.io diff --git a/doc/source/conf.py b/doc/source/conf.py index 8fec513c..46770d5c 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/regenerate.sh b/regenerate.sh index 0efc51a4..47f9c962 100755 --- a/regenerate.sh +++ b/regenerate.sh @@ -6,16 +6,12 @@ dropdb taiga echo "-> Create taiga DB" createdb taiga -echo "-> Run syncdb" +echo "-> Load migrations" python manage.py migrate -# echo "-> Load initial Site" -# python manage.py loaddata initial_site --traceback -echo "-> Load initial user" +echo "-> Load initial user (admin/123123)" python manage.py loaddata initial_user --traceback -echo "-> Load initial project_templates" +echo "-> Load initial project_templates (scrum/kanban)" python manage.py loaddata initial_project_templates --traceback -echo "-> Load initial roles" -python manage.py loaddata initial_role --traceback echo "-> Generate sample data" python manage.py sample_data --traceback echo "-> Rebuilding timeline" diff --git a/requirements-devel.txt b/requirements-devel.txt index df6cacac..cccd2a5e 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,13 +1,13 @@ -r requirements.txt -factory_boy==2.4.1 -py==1.4.26 -pytest==2.6.4 -pytest-django==2.8.0 -pytest-pythonpath==0.6 +factory_boy==2.6.0 +py==1.4.30 +pytest==2.8.2 +pytest-django==2.9.1 +pytest-pythonpath==0.7 -coverage==3.7.1 -coveralls==0.4.2 +coverage==4.0.1 +coveralls==1.1 django-slowdown==0.0.1 transifex-client==0.11.1.beta diff --git a/requirements.txt b/requirements.txt index 0098b044..533a20e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,36 +1,35 @@ -Django==1.7.8 +Django==1.8.5 #djangorestframework==2.3.13 # It's not necessary since Taiga 1.7 -django-picklefield==0.3.1 -django-sampledatahelper==0.2.2 +django-picklefield==0.3.2 +django-sampledatahelper==0.3.0 gunicorn==19.3.0 -psycopg2==2.5.4 -pillow==2.5.3 -pytz==2014.4 -six==1.8.0 -amqp==1.4.6 -djmail==0.10 -django-pgjson==0.2.2 -djorm-pgarray==1.0.4 -django-jinja==1.0.4 -jinja2==2.7.2 -pygments==1.6 +psycopg2==2.6.1 +Pillow==3.0.0 +pytz==2015.7 +six==1.10.0 +amqp==1.4.7 +djmail==0.11 +django-pgjson==0.3.1 +djorm-pgarray==1.2 +django-jinja==1.4.1 +jinja2==2.8 +pygments==2.0.2 django-sites==0.8 -Markdown==2.4.1 -fn==0.2.13 +Markdown==2.6.3 +fn==0.4.3 diff-match-patch==20121119 -requests==2.4.1 +requests==2.8.1 django-sr==0.0.4 -easy-thumbnails==2.1 -celery==3.1.17 +easy-thumbnails==2.2 +celery==3.1.19 redis==2.10.3 -Unidecode==0.04.16 -raven==5.1.1 -bleach==1.4 -django-ipware==0.1.0 -premailer==2.8.1 -django-transactional-cleanup==0.1.14 -lxml==3.4.1 +Unidecode==0.04.18 +raven==5.8.1 +bleach==1.4.2 +django-ipware==1.1.1 +premailer==2.9.6 +cssutils==1.0.1 # Compatible with python 3.5 +django-transactional-cleanup==0.1.15 +lxml==3.5.0b1 git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea - -# Comment it if you are using python >= 3.4 -enum34==1.0 +pyjwkest==1.0.7 diff --git a/scripts/manage_translations.py b/scripts/manage_translations.py index 22bcccbc..c7a2d1ff 100644 --- a/scripts/manage_translations.py +++ b/scripts/manage_translations.py @@ -84,6 +84,7 @@ def update_catalogs(resources=None, languages=None): cmd = makemessages.Command() opts = { "locale": ["en"], + "exclude": [], "extensions": ["py", "jinja"], # Default values @@ -96,7 +97,7 @@ def update_catalogs(resources=None, languages=None): "no_location": False, "no_obsolete": False, "keep_pot": False, - "verbosity": "0", + "verbosity": 0, } if resources is not None: diff --git a/settings/__init__.py b/settings/__init__.py index dd336c53..fb18d127 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/settings/celery.py b/settings/celery.py index 59c8d51a..70cd1095 100644 --- a/settings/celery.py +++ b/settings/celery.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/settings/common.py b/settings/common.py index cc1c69cd..355a6597 100644 --- a/settings/common.py +++ b/settings/common.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -25,6 +25,8 @@ ADMINS = ( ("Admin", "example@example.com"), ) +DEBUG = False + DATABASES = { "default": { "ENGINE": "transaction_hooks.backends.postgresql_psycopg2", @@ -105,7 +107,7 @@ LANGUAGES = [ #("id", "Bahasa Indonesia"), # Indonesian #("io", "IDO"), # Ido #("is", "Íslenska"), # Icelandic - #("it", "Italiano"), # Italian + ("it", "Italiano"), # Italian #("ja", "日本語"), # Japanese #("ka", "ქართული"), # Georgian #("kk", "Қазақша"), # Kazakh @@ -126,11 +128,11 @@ LANGUAGES = [ #("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk #("os", "Ирон æвзаг"), # Ossetic #("pa", "ਪੰਜਾਬੀ"), # Punjabi - #("pl", "Polski"), # Polish + ("pl", "Polski"), # Polish #("pt", "Português (Portugal)"), # Portuguese - #("pt-br", "Português (Brasil)"), # Brazilian Portuguese + ("pt-br", "Português (Brasil)"), # Brazilian Portuguese #("ro", "Română"), # Romanian - #("ru", "Русский"), # Russian + ("ru", "Русский"), # Russian #("sk", "Slovenčina"), # Slovak #("sl", "Slovenščina"), # Slovenian #("sq", "Shqip"), # Albanian @@ -191,9 +193,6 @@ MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage" # urls depends on it. On production should be set # something like https://media.taiga.io/ MEDIA_URL = "http://localhost:8000/media/" - -# Static url is not widelly used by taiga (only -# if admin is activated). STATIC_URL = "http://localhost:8000/static/" # Static configuration. @@ -215,11 +214,47 @@ DEFAULT_FILE_STORAGE = "taiga.base.storage.FileSystemStorage" SECRET_KEY = "aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e" -TEMPLATE_LOADERS = [ - "django_jinja.loaders.AppLoader", - "django_jinja.loaders.FileSystemLoader", +TEMPLATES = [ + { + "BACKEND": "django_jinja.backend.Jinja2", + "DIRS": [ + os.path.join(BASE_DIR, "templates"), + ], + "APP_DIRS": True, + "OPTIONS": { + 'context_processors': [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + ], + "match_extension": ".jinja", + } + }, + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(BASE_DIR, "templates"), + ], + "APP_DIRS": True, + "OPTIONS": { + 'context_processors': [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + ], + } + }, ] + MIDDLEWARE_CLASSES = [ "taiga.base.middleware.cors.CoorsMiddleware", "taiga.events.middleware.SessionIDMiddleware", @@ -234,22 +269,9 @@ MIDDLEWARE_CLASSES = [ "django.contrib.messages.middleware.MessageMiddleware", ] -TEMPLATE_CONTEXT_PROCESSORS = [ - "django.contrib.auth.context_processors.auth", - "django.core.context_processors.request", - "django.core.context_processors.i18n", - "django.core.context_processors.media", - "django.core.context_processors.static", - "django.core.context_processors.tz", - "django.contrib.messages.context_processors.messages", -] ROOT_URLCONF = "taiga.urls" -TEMPLATE_DIRS = [ - os.path.join(BASE_DIR, "templates"), -] - INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", @@ -266,12 +288,14 @@ INSTALLED_APPS = [ "taiga.front", "taiga.users", "taiga.userstorage", + "taiga.external_apps", "taiga.projects", "taiga.projects.references", "taiga.projects.custom_attributes", "taiga.projects.history", "taiga.projects.notifications", "taiga.projects.attachments", + "taiga.projects.likes", "taiga.projects.votes", "taiga.projects.milestones", "taiga.projects.userstories", @@ -384,6 +408,9 @@ REST_FRAMEWORK = { # Mainly used for api debug. "taiga.auth.backends.Session", + + # Application tokens auth + "taiga.external_apps.auth_backends.Token", ), "DEFAULT_THROTTLE_CLASSES": ( "taiga.base.throttling.AnonRateThrottle", @@ -403,6 +430,11 @@ REST_FRAMEWORK = { "DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%S%z" } +# 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" +] DEFAULT_PROJECT_TEMPLATE = "scrum" PUBLIC_REGISTER_ENABLED = False diff --git a/settings/development.py b/settings/development.py index 008c128b..1a77df9d 100644 --- a/settings/development.py +++ b/settings/development.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,8 +17,5 @@ from .common import * DEBUG = True -TEMPLATE_DEBUG = DEBUG -TEMPLATE_CONTEXT_PROCESSORS += [ - "django.core.context_processors.debug", -] +TEMPLATES[0]["OPTIONS"]['context_processors'] += "django.template.context_processors.debug" diff --git a/settings/local.py.example b/settings/local.py.example index 62a88868..b6bcf2b9 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,6 +16,12 @@ from .development import * +#DEBUG = False + +#ADMINS = ( +# ("Admin", "example@example.com"), +#) + DATABASES = { 'default': { 'ENGINE': 'transaction_hooks.backends.postgresql_psycopg2', @@ -27,11 +33,25 @@ DATABASES = { } } -#HOST="http://taiga.projects.kaleidos.net" -# +#SITES = { +# "api": { +# "scheme": "http", +# "domain": "localhost:8000", +# "name": "api" +# }, +# "front": { +# "scheme": "http", +# "domain": "localhost:9001", +# "name": "front" +# }, +#} + +#SITE_ID = "api" + #MEDIA_ROOT = '/home/taiga/media' #STATIC_ROOT = '/home/taiga/static' + # EMAIL SETTINGS EXAMPLE #EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #EMAIL_USE_TLS = False diff --git a/settings/sr.py b/settings/sr.py index cd1bc113..9c523878 100644 --- a/settings/sr.py +++ b/settings/sr.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/settings/testing.py b/settings/testing.py index 9fb6ec74..01eff7c5 100644 --- a/settings/testing.py +++ b/settings/testing.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/settings/travis.py b/settings/travis.py index 6f27652f..20920546 100644 --- a/settings/travis.py +++ b/settings/travis.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/__init__.py b/taiga/__init__.py index b62e0bd0..721fcd42 100644 --- a/taiga/__init__.py +++ b/taiga/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/auth/api.py b/taiga/auth/api.py index a666dc2e..c70e7e7f 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py index fe44544b..d2f71553 100644 --- a/taiga/auth/backends.py +++ b/taiga/auth/backends.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/auth/permissions.py b/taiga/auth/permissions.py index c0e457b3..7fe0d452 100644 --- a/taiga/auth/permissions.py +++ b/taiga/auth/permissions.py @@ -1,5 +1,5 @@ -# Copyright (C) 2014 Andrey Antukh # Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh # Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py index 2073e000..42b077e7 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 952bf6a7..73b65b5d 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -28,9 +28,8 @@ from django.db import transaction as tx from django.db import IntegrityError from django.utils.translation import ugettext as _ -from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail - from taiga.base import exceptions as exc +from taiga.base.mails import mail_builder from taiga.users.serializers import UserAdminSerializer from taiga.users.services import get_and_validate_user @@ -57,8 +56,7 @@ def send_register_email(user) -> bool: """ cancel_token = get_token_for_user(user, "cancel_account") context = {"user": user, "cancel_token": cancel_token} - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - email = mbuilder.registered_user(user, context) + email = mail_builder.registered_user(user, context) return bool(email.send()) diff --git a/taiga/auth/signals.py b/taiga/auth/signals.py index 9c5b9ca0..2f674fe1 100644 --- a/taiga/auth/signals.py +++ b/taiga/auth/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py index f113ba8a..a24b91e4 100644 --- a/taiga/auth/tokens.py +++ b/taiga/auth/tokens.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/__init__.py b/taiga/base/__init__.py index d6006178..6aa21c11 100644 --- a/taiga/base/__init__.py +++ b/taiga/base/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/__init__.py b/taiga/base/api/__init__.py index 69083fa5..f8457cd1 100644 --- a/taiga/base/api/__init__.py +++ b/taiga/base/api/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -31,9 +31,11 @@ from .viewsets import ModelCrudViewSet from .viewsets import ModelUpdateRetrieveViewSet from .viewsets import GenericViewSet from .viewsets import ReadOnlyListViewSet +from .viewsets import ModelRetrieveViewSet __all__ = ["ModelCrudViewSet", "ModelListViewSet", "ModelUpdateRetrieveViewSet", "GenericViewSet", - "ReadOnlyListViewSet"] + "ReadOnlyListViewSet", + "ModelRetrieveViewSet"] diff --git a/taiga/base/api/authentication.py b/taiga/base/api/authentication.py index 8343fa30..ad83bdbf 100644 --- a/taiga/base/api/authentication.py +++ b/taiga/base/api/authentication.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index 942878f7..ad05e422 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index feeebf77..2315bfc1 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -85,7 +85,7 @@ class GenericAPIView(pagination.PaginationMixin, many=many, partial=partial, context=context) - def filter_queryset(self, queryset): + def filter_queryset(self, queryset, filter_backends=None): """ Given a queryset, filter it with whichever filter backend is in use. @@ -94,7 +94,10 @@ class GenericAPIView(pagination.PaginationMixin, method if you want to apply the configured filtering backend to the default queryset. """ - for backend in self.get_filter_backends(): + #NOTE TAIGA: Added filter_backends to overwrite the default behavior. + + backends = filter_backends or self.get_filter_backends() + for backend in backends: queryset = backend().filter_queryset(self.request, queryset, self) return queryset diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index 5576db90..371b44c7 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/negotiation.py b/taiga/base/api/negotiation.py index 60278752..f4984a11 100644 --- a/taiga/base/api/negotiation.py +++ b/taiga/base/api/negotiation.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/pagination.py b/taiga/base/api/pagination.py index 028d8106..dbab110b 100644 --- a/taiga/base/api/pagination.py +++ b/taiga/base/api/pagination.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/parsers.py b/taiga/base/api/parsers.py index 1465f601..3b254633 100644 --- a/taiga/base/api/parsers.py +++ b/taiga/base/api/parsers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py index c4e6917d..54e3be02 100644 --- a/taiga/base/api/permissions.py +++ b/taiga/base/api/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/relations.py b/taiga/base/api/relations.py index 87fbfb4c..4c02ec91 100644 --- a/taiga/base/api/relations.py +++ b/taiga/base/api/relations.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/renderers.py b/taiga/base/api/renderers.py index 2da7ed4c..c30cb074 100644 --- a/taiga/base/api/renderers.py +++ b/taiga/base/api/renderers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/request.py b/taiga/base/api/request.py index c57d29fc..0e5fe48a 100644 --- a/taiga/base/api/request.py +++ b/taiga/base/api/request.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/reverse.py b/taiga/base/api/reverse.py index 71c7c047..1049388f 100644 --- a/taiga/base/api/reverse.py +++ b/taiga/base/api/reverse.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 8add00ba..3f33e180 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -1005,7 +1005,7 @@ class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer))) m2m_data[field_name] = attrs.pop(field_name) # Forward m2m relations - for field in meta.many_to_many + meta.virtual_fields: + for field in list(meta.many_to_many) + meta.virtual_fields: if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py index 34dd9717..5eda4866 100644 --- a/taiga/base/api/settings.py +++ b/taiga/base/api/settings.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/static/api/css/bootstrap-tweaks.css b/taiga/base/api/static/api/css/bootstrap-tweaks.css index b1cd265b..98b0348f 100644 --- a/taiga/base/api/static/api/css/bootstrap-tweaks.css +++ b/taiga/base/api/static/api/css/bootstrap-tweaks.css @@ -1,7 +1,7 @@ /* - * Copyright (C) 2015 Andrey Antukh - * Copyright (C) 2015 Jesús Espino - * Copyright (C) 2015 David Barragán + * Copyright (C) 2014-2015 Andrey Antukh + * Copyright (C) 2014-2015 Jesús Espino + * Copyright (C) 2014-2015 David Barragán * 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 diff --git a/taiga/base/api/static/api/css/default.css b/taiga/base/api/static/api/css/default.css index d7c5722e..cbd7191c 100644 --- a/taiga/base/api/static/api/css/default.css +++ b/taiga/base/api/static/api/css/default.css @@ -1,7 +1,7 @@ /* - * Copyright (C) 2015 Andrey Antukh - * Copyright (C) 2015 Jesús Espino - * Copyright (C) 2015 David Barragán + * Copyright (C) 2014-2015 Andrey Antukh + * Copyright (C) 2014-2015 Jesús Espino + * Copyright (C) 2014-2015 David Barragán * 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 diff --git a/taiga/base/api/static/api/css/prettify.css b/taiga/base/api/static/api/css/prettify.css index c037439d..1511794c 100644 --- a/taiga/base/api/static/api/css/prettify.css +++ b/taiga/base/api/static/api/css/prettify.css @@ -1,7 +1,7 @@ /* - * Copyright (C) 2015 Andrey Antukh - * Copyright (C) 2015 Jesús Espino - * Copyright (C) 2015 David Barragán + * Copyright (C) 2014-2015 Andrey Antukh + * Copyright (C) 2014-2015 Jesús Espino + * Copyright (C) 2014-2015 David Barragán * 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 diff --git a/taiga/base/api/static/api/js/default.js b/taiga/base/api/static/api/js/default.js index 3e9b9e3c..fc8563d2 100644 --- a/taiga/base/api/static/api/js/default.js +++ b/taiga/base/api/static/api/js/default.js @@ -1,7 +1,7 @@ /* - * Copyright (C) 2015 Andrey Antukh - * Copyright (C) 2015 Jesús Espino - * Copyright (C) 2015 David Barragán + * Copyright (C) 2014-2015 Andrey Antukh + * Copyright (C) 2014-2015 Jesús Espino + * Copyright (C) 2014-2015 David Barragán * 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 diff --git a/taiga/base/api/static/api/js/prettify-min.js b/taiga/base/api/static/api/js/prettify-min.js index 54d16bac..c225269f 100644 --- a/taiga/base/api/static/api/js/prettify-min.js +++ b/taiga/base/api/static/api/js/prettify-min.js @@ -1,7 +1,7 @@ /* - * Copyright (C) 2015 Andrey Antukh - * Copyright (C) 2015 Jesús Espino - * Copyright (C) 2015 David Barragán + * Copyright (C) 2014-2015 Andrey Antukh + * Copyright (C) 2014-2015 Jesús Espino + * Copyright (C) 2014-2015 David Barragán * 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 diff --git a/taiga/base/api/templatetags/api.py b/taiga/base/api/templatetags/api.py index 987cb90b..642b476d 100644 --- a/taiga/base/api/templatetags/api.py +++ b/taiga/base/api/templatetags/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/throttling.py b/taiga/base/api/throttling.py index bec43b87..89cf4c7d 100644 --- a/taiga/base/api/throttling.py +++ b/taiga/base/api/throttling.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/urlpatterns.py b/taiga/base/api/urlpatterns.py index a48fa572..4c67ad23 100644 --- a/taiga/base/api/urlpatterns.py +++ b/taiga/base/api/urlpatterns.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/urls.py b/taiga/base/api/urls.py index 0e1f61bf..e7e4af2b 100644 --- a/taiga/base/api/urls.py +++ b/taiga/base/api/urls.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/utils.py b/taiga/base/api/utils.py index 433e668a..8f803b0a 100644 --- a/taiga/base/api/utils.py +++ b/taiga/base/api/utils.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/utils/__init__.py b/taiga/base/api/utils/__init__.py index d215bf8f..a555ba83 100644 --- a/taiga/base/api/utils/__init__.py +++ b/taiga/base/api/utils/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/utils/breadcrumbs.py b/taiga/base/api/utils/breadcrumbs.py index 39fa383f..950fe710 100644 --- a/taiga/base/api/utils/breadcrumbs.py +++ b/taiga/base/api/utils/breadcrumbs.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/utils/encoders.py b/taiga/base/api/utils/encoders.py index 29dc36d3..cc998d24 100644 --- a/taiga/base/api/utils/encoders.py +++ b/taiga/base/api/utils/encoders.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/utils/formatting.py b/taiga/base/api/utils/formatting.py index c9940975..c8a1781e 100644 --- a/taiga/base/api/utils/formatting.py +++ b/taiga/base/api/utils/formatting.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/utils/mediatypes.py b/taiga/base/api/utils/mediatypes.py index dce7b06a..b049f166 100644 --- a/taiga/base/api/utils/mediatypes.py +++ b/taiga/base/api/utils/mediatypes.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py index 7a751eaf..0d5a60da 100644 --- a/taiga/base/api/views.py +++ b/taiga/base/api/views.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -447,7 +447,7 @@ class APIView(View): def api_server_error(request, *args, **kwargs): - if settings.DEBUG is False and request.META['CONTENT_TYPE'] == "application/json": + if settings.DEBUG is False and request.META.get('CONTENT_TYPE', None) == "application/json": return HttpResponse(json.dumps({"error": _("Server application error")}), status=status.HTTP_500_INTERNAL_SERVER_ERROR) return server_error(request, *args, **kwargs) diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py index dd56cd14..9122972a 100644 --- a/taiga/base/api/viewsets.py +++ b/taiga/base/api/viewsets.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -167,3 +167,7 @@ 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 7125cbe7..32ef371c 100644 --- a/taiga/base/apps.py +++ b/taiga/base/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/connectors/exceptions.py b/taiga/base/connectors/exceptions.py index 1619abba..7173c757 100644 --- a/taiga/base/connectors/exceptions.py +++ b/taiga/base/connectors/exceptions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py index d11b97f9..37912397 100644 --- a/taiga/base/decorators.py +++ b/taiga/base/decorators.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index 7adb31f9..a6e3850f 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/fields.py b/taiga/base/fields.py index a1a8c56c..0c5d96aa 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -110,3 +110,12 @@ class TagsColorsField(serializers.WritableField): def from_native(self, data): return list(data.items()) + + + +class WatchersField(serializers.WritableField): + def to_native(self, obj): + return obj + + def from_native(self, data): + return data diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 208be999..a6cf64e3 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -18,17 +18,23 @@ from functools import reduce import logging from django.apps import apps +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import ugettext as _ from taiga.base import exceptions as exc from taiga.base.api.utils import get_object_or_404 - +from taiga.base.utils.db import to_tsquery logger = logging.getLogger(__name__) +##################################################################### +# Base and Mixins +##################################################################### + + class BaseFilterBackend(object): """ A base class from which all filter backend classes should inherit. @@ -95,6 +101,9 @@ class OrderByFilterMixin(QueryParamsFilterMixin): if field_name not in order_by_fields: return queryset + if raw_fieldname in ["owner", "-owner", "assigned_to", "-assigned_to"]: + raw_fieldname = "{}__full_name".format(raw_fieldname) + return super().filter_queryset(request, queryset.order_by(raw_fieldname), view) @@ -105,6 +114,10 @@ class FilterBackend(OrderByFilterMixin): pass +##################################################################### +# Permissions filters +##################################################################### + class PermissionBasedFilterBackend(FilterBackend): permission = None @@ -345,9 +358,84 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi return super().filter_queryset(request, queryset, view) +##################################################################### +# Generic Attributes filters +##################################################################### + +class BaseRelatedFieldsFilter(FilterBackend): + def __init__(self, filter_name=None): + if filter_name: + self.filter_name = filter_name + + def _prepare_filter_data(self, query_param_value): + def _transform_value(value): + try: + return int(value) + except: + if value in self._special_values_dict: + return self._special_values_dict[value] + raise exc.BadRequest() + + values = set([x.strip() for x in query_param_value.split(",")]) + values = map(_transform_value, values) + return list(values) + + def _get_queryparams(self, params): + raw_value = params.get(self.filter_name, None) + + if raw_value: + value = self._prepare_filter_data(raw_value) + + if None in value: + qs_in_kwargs = {"{}__in".format(self.filter_name): [v for v in value if v is not None]} + qs_isnull_kwargs = {"{}__isnull".format(self.filter_name): True} + return Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs) + else: + return {"{}__in".format(self.filter_name): value} + + return None + + def filter_queryset(self, request, queryset, view): + query = self._get_queryparams(request.QUERY_PARAMS) + if query: + if isinstance(query, dict): + queryset = queryset.filter(**query) + else: + queryset = queryset.filter(query) + + return super().filter_queryset(request, queryset, view) + + +class OwnersFilter(BaseRelatedFieldsFilter): + filter_name = 'owner' + + +class AssignedToFilter(BaseRelatedFieldsFilter): + filter_name = 'assigned_to' + + +class StatusesFilter(BaseRelatedFieldsFilter): + filter_name = 'status' + + +class IssueTypesFilter(BaseRelatedFieldsFilter): + filter_name = 'type' + + +class PrioritiesFilter(BaseRelatedFieldsFilter): + filter_name = 'priority' + + +class SeveritiesFilter(BaseRelatedFieldsFilter): + filter_name = 'severity' + + class TagsFilter(FilterBackend): - def __init__(self, filter_name='tags'): - self.filter_name = filter_name + filter_name = 'tags' + + def __init__(self, filter_name=None): + if filter_name: + self.filter_name = filter_name def _get_tags_queryparams(self, params): tags = params.get(self.filter_name, None) @@ -364,35 +452,46 @@ class TagsFilter(FilterBackend): return super().filter_queryset(request, queryset, view) -class StatusFilter(FilterBackend): - def __init__(self, filter_name='status'): - self.filter_name = filter_name - def _get_status_queryparams(self, params): - status = params.get(self.filter_name, None) - if status is not None: - status = set([x.strip() for x in status.split(",")]) - return list(status) +class WatchersFilter(FilterBackend): + filter_name = 'watchers' + + def __init__(self, filter_name=None): + if filter_name: + self.filter_name = filter_name + + def _get_watchers_queryparams(self, params): + watchers = params.get(self.filter_name, None) + if watchers: + return watchers.split(",") return None def filter_queryset(self, request, queryset, view): - query_status = self._get_status_queryparams(request.QUERY_PARAMS) - if query_status: - queryset = queryset.filter(status__in=query_status) + query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS) + model = queryset.model + if query_watchers: + WatchedModel = apps.get_model("notifications", "Watched") + watched_type = ContentType.objects.get_for_model(queryset.model) + watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True) + queryset = queryset.filter(id__in=watched_ids) return super().filter_queryset(request, queryset, view) +##################################################################### +# Text search filters +##################################################################### + class QFilter(FilterBackend): def filter_queryset(self, request, queryset, view): q = request.QUERY_PARAMS.get('q', None) if q: - if q.isdigit(): - qs_args = [Q(ref=q)] - else: - qs_args = [Q(subject__icontains=x) for x in q.split()] + table = queryset.model._meta.db_table + where_clause = ("to_tsvector('english_nostop', coalesce({table}.subject, '') || ' ' || " + "coalesce({table}.ref) || ' ' || " + "coalesce({table}.description, '')) @@ to_tsquery('english_nostop', %s)".format(table=table)) - queryset = queryset.filter(reduce(operator.and_, qs_args)) + queryset = queryset.extra(where=[where_clause], params=[to_tsquery(q)]) return queryset diff --git a/taiga/base/formats/en/formats.py b/taiga/base/formats/en/formats.py index 959dba6d..6f5a83d4 100644 --- a/taiga/base/formats/en/formats.py +++ b/taiga/base/formats/en/formats.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/formats/es/formats.py b/taiga/base/formats/es/formats.py index 8fc0d277..a6c8e8f5 100644 --- a/taiga/base/formats/es/formats.py +++ b/taiga/base/formats/es/formats.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/mails.py b/taiga/base/mails.py new file mode 100644 index 00000000..ac0517e0 --- /dev/null +++ b/taiga/base/mails.py @@ -0,0 +1,42 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings + +from djmail import template_mail +import premailer + +import logging + + +# Hide CSS warnings messages if debug mode is disable +if not getattr(settings, "DEBUG", False): + premailer.premailer.cssutils.log.setLevel(logging.CRITICAL) + + +class InlineCSSTemplateMail(template_mail.TemplateMail): + def _render_message_body_as_html(self, context): + html = super()._render_message_body_as_html(context) + + # Transform CSS into line style attributes + return premailer.transform(html) + + +class MagicMailBuilder(template_mail.MagicMailBuilder): + pass + + +mail_builder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py index da535c7f..c6d20a7d 100644 --- a/taiga/base/management/commands/test_emails.py +++ b/taiga/base/management/commands/test_emails.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -22,7 +22,7 @@ from django.db.models.loading import get_model from django.core.management.base import BaseCommand from django.utils import timezone -from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail +from taiga.base.mails import mail_builder from taiga.projects.models import Project, Membership from taiga.projects.history.models import HistoryEntry @@ -47,11 +47,12 @@ class Command(BaseCommand): locale = options.get('locale') test_email = args[0] - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - # Register email - context = {"lang": locale, "user": User.objects.all().order_by("?").first(), "cancel_token": "cancel-token"} - email = mbuilder.registered_user(test_email, context) + context = {"lang": locale, + "user": User.objects.all().order_by("?").first(), + "cancel_token": "cancel-token"} + + email = mail_builder.registered_user(test_email, context) email.send() # Membership invitation @@ -60,12 +61,13 @@ class Command(BaseCommand): membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example" context = {"lang": locale, "membership": membership} - email = mbuilder.membership_invitation(test_email, context) + email = mail_builder.membership_invitation(test_email, context) email.send() # Membership notification - context = {"lang": locale, "membership": Membership.objects.order_by("?").filter(user__isnull=False).first()} - email = mbuilder.membership_notification(test_email, context) + context = {"lang": locale, + "membership": Membership.objects.order_by("?").filter(user__isnull=False).first()} + email = mail_builder.membership_notification(test_email, context) email.send() # Feedback @@ -81,17 +83,17 @@ class Command(BaseCommand): "key2": "value2", }, } - email = mbuilder.feedback_notification(test_email, context) + email = mail_builder.feedback_notification(test_email, context) email.send() # Password recovery context = {"lang": locale, "user": User.objects.all().order_by("?").first()} - email = mbuilder.password_recovery(test_email, context) + email = mail_builder.password_recovery(test_email, context) email.send() # Change email context = {"lang": locale, "user": User.objects.all().order_by("?").first()} - email = mbuilder.change_email(test_email, context) + email = mail_builder.change_email(test_email, context) email.send() # Export/Import emails @@ -102,7 +104,7 @@ class Command(BaseCommand): "error_subject": "Error generating project dump", "error_message": "Error generating project dump", } - email = mbuilder.export_error(test_email, context) + email = mail_builder.export_error(test_email, context) email.send() context = { "lang": locale, @@ -110,7 +112,7 @@ class Command(BaseCommand): "error_subject": "Error importing project dump", "error_message": "Error importing project dump", } - email = mbuilder.import_error(test_email, context) + email = mail_builder.import_error(test_email, context) email.send() deletion_date = timezone.now() + datetime.timedelta(seconds=60*60*24) @@ -121,7 +123,7 @@ class Command(BaseCommand): "project": Project.objects.all().order_by("?").first(), "deletion_date": deletion_date, } - email = mbuilder.dump_project(test_email, context) + email = mail_builder.dump_project(test_email, context) email.send() context = { @@ -129,7 +131,7 @@ class Command(BaseCommand): "user": User.objects.all().order_by("?").first(), "project": Project.objects.all().order_by("?").first(), } - email = mbuilder.load_dump(test_email, context) + email = mail_builder.load_dump(test_email, context) email.send() # Notification emails diff --git a/taiga/base/middleware/cors.py b/taiga/base/middleware/cors.py index 86d6b5c5..b4ed438f 100644 --- a/taiga/base/middleware/cors.py +++ b/taiga/base/middleware/cors.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,6 +15,7 @@ # along with this program. If not, see . from django import http +from django.conf import settings COORS_ALLOWED_ORIGINS = "*" @@ -28,13 +29,15 @@ COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by", "x-pagination-current", "x-pagination-next", "x-pagination-prev", "x-site-host", "x-site-register"] +COORS_EXTRA_EXPOSE_HEADERS = getattr(settings, "APP_EXTRA_EXPOSE_HEADERS", []) + class CoorsMiddleware(object): def _populate_response(self, response): response["Access-Control-Allow-Origin"] = COORS_ALLOWED_ORIGINS response["Access-Control-Allow-Methods"] = ",".join(COORS_ALLOWED_METHODS) response["Access-Control-Allow-Headers"] = ",".join(COORS_ALLOWED_HEADERS) - response["Access-Control-Expose-Headers"] = ",".join(COORS_EXPOSE_HEADERS) + response["Access-Control-Expose-Headers"] = ",".join(COORS_EXPOSE_HEADERS + COORS_EXTRA_EXPOSE_HEADERS) response["Access-Control-Max-Age"] = "3600" if COORS_ALLOWED_CREDENTIALS: diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py index 4e77c9cc..0a23f2d6 100644 --- a/taiga/base/neighbors.py +++ b/taiga/base/neighbors.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -43,7 +43,7 @@ def get_neighbors(obj, results_set=None): query = """ SELECT * FROM - (SELECT "id" as id, ROW_NUMBER() OVER() + (SELECT "col1" as id, ROW_NUMBER() OVER() FROM (%s) as ID_AND_ROW) AS SELECTED_ID_AND_ROW """ % (base_sql) diff --git a/taiga/base/response.py b/taiga/base/response.py index 47fc6641..458411c7 100644 --- a/taiga/base/response.py +++ b/taiga/base/response.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/base/routers.py b/taiga/base/routers.py index c792771c..6b72826c 100644 --- a/taiga/base/routers.py +++ b/taiga/base/routers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/status.py b/taiga/base/status.py index 5fb0a4cf..0000145c 100644 --- a/taiga/base/status.py +++ b/taiga/base/status.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/storage.py b/taiga/base/storage.py index 35e234df..ad79c9fd 100644 --- a/taiga/base/storage.py +++ b/taiga/base/storage.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/tags.py b/taiga/base/tags.py index 4b7c6409..9af3cfe6 100644 --- a/taiga/base/tags.py +++ b/taiga/base/tags.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/base/throttling.py b/taiga/base/throttling.py index 268a397d..edc1fa14 100644 --- a/taiga/base/throttling.py +++ b/taiga/base/throttling.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/contenttypes.py b/taiga/base/utils/contenttypes.py new file mode 100644 index 00000000..a475b352 --- /dev/null +++ b/taiga/base/utils/contenttypes.py @@ -0,0 +1,23 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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.contrib.contenttypes.management import update_contenttypes + + +def update_all_contenttypes(**kwargs): + for app_config in apps.get_app_configs(): + update_contenttypes(app_config, **kwargs) diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py index a9663751..f71b10f3 100644 --- a/taiga/base/utils/db.py +++ b/taiga/base/utils/db.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,10 +16,28 @@ from django.contrib.contenttypes.models import ContentType from django.db import transaction +from django.shortcuts import _get_queryset from . import functions +def get_object_or_none(klass, *args, **kwargs): + """ + Uses get() to return an object, or None if the object does not exist. + + klass may be a Model, Manager, or QuerySet object. All other passed + arguments and keyword arguments are used in the get() query. + + Note: Like with get(), an MultipleObjectsReturned will be raised if more + than one object is found. + """ + queryset = _get_queryset(klass) + try: + return queryset.get(*args, **kwargs) + except queryset.model.DoesNotExist: + return None + + def get_typename_for_model_class(model:object, for_concrete_model=True) -> str: """ Get typename for model instance. @@ -107,3 +125,9 @@ def update_in_bulk_with_ids(ids, list_of_new_values, model): """ for id, new_values in zip(ids, list_of_new_values): model.objects.filter(id=id).update(**new_values) + + +def to_tsquery(text): + # We want to transform a query like "exam proj" (should find "project example") to something like proj:* & exam:* + search_elems = ["{}:*".format(search_elem) for search_elem in text.split(" ")] + return " & ".join(search_elems) diff --git a/taiga/base/utils/dicts.py b/taiga/base/utils/dicts.py index 512a044d..4a3c1ce6 100644 --- a/taiga/base/utils/dicts.py +++ b/taiga/base/utils/dicts.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/diff.py b/taiga/base/utils/diff.py index 7c8ea034..27f1281e 100644 --- a/taiga/base/utils/diff.py +++ b/taiga/base/utils/diff.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/functions.py b/taiga/base/utils/functions.py index d20f824a..3570a59a 100644 --- a/taiga/base/utils/functions.py +++ b/taiga/base/utils/functions.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/base/utils/iterators.py b/taiga/base/utils/iterators.py index fff3a9de..f7249112 100644 --- a/taiga/base/utils/iterators.py +++ b/taiga/base/utils/iterators.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/base/utils/json.py b/taiga/base/utils/json.py index 40132b34..d9e54132 100644 --- a/taiga/base/utils/json.py +++ b/taiga/base/utils/json.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/sequence.py b/taiga/base/utils/sequence.py index 18af3e2f..da50953a 100644 --- a/taiga/base/utils/sequence.py +++ b/taiga/base/utils/sequence.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/signals.py b/taiga/base/utils/signals.py index 1fe9c071..d2700790 100644 --- a/taiga/base/utils/signals.py +++ b/taiga/base/utils/signals.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/base/utils/slug.py b/taiga/base/utils/slug.py index 03b95767..48776ac3 100644 --- a/taiga/base/utils/slug.py +++ b/taiga/base/utils/slug.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/text.py b/taiga/base/utils/text.py index df9b0259..b8de6aef 100644 --- a/taiga/base/utils/text.py +++ b/taiga/base/utils/text.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/base/utils/urls.py b/taiga/base/utils/urls.py index 2cd5f067..e13d783e 100644 --- a/taiga/base/utils/urls.py +++ b/taiga/base/utils/urls.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/celery.py b/taiga/celery.py index ef9b7d06..8084290b 100644 --- a/taiga/celery.py +++ b/taiga/celery.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/contrib_routers.py b/taiga/contrib_routers.py index 311f96c3..129d56b6 100644 --- a/taiga/contrib_routers.py +++ b/taiga/contrib_routers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/deferred.py b/taiga/deferred.py index 62080a77..084a16f0 100644 --- a/taiga/deferred.py +++ b/taiga/deferred.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/events/__init__.py b/taiga/events/__init__.py index bc6d8fa2..b2d6c236 100644 --- a/taiga/events/__init__.py +++ b/taiga/events/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/events/apps.py b/taiga/events/apps.py index 40b51834..1081d6db 100644 --- a/taiga/events/apps.py +++ b/taiga/events/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/events/backends/__init__.py b/taiga/events/backends/__init__.py index f72b8c5b..da0d1ba3 100644 --- a/taiga/events/backends/__init__.py +++ b/taiga/events/backends/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/events/backends/base.py b/taiga/events/backends/base.py index 4eefcb55..16189070 100644 --- a/taiga/events/backends/base.py +++ b/taiga/events/backends/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 diff --git a/taiga/events/backends/postgresql.py b/taiga/events/backends/postgresql.py index 696a0813..beaf04ee 100644 --- a/taiga/events/backends/postgresql.py +++ b/taiga/events/backends/postgresql.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 diff --git a/taiga/events/backends/rabbitmq.py b/taiga/events/backends/rabbitmq.py index 182d2548..18b573b1 100644 --- a/taiga/events/backends/rabbitmq.py +++ b/taiga/events/backends/rabbitmq.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 diff --git a/taiga/events/events.py b/taiga/events/events.py index 26343694..3bd29173 100644 --- a/taiga/events/events.py +++ b/taiga/events/events.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 diff --git a/taiga/events/middleware.py b/taiga/events/middleware.py index 9dbfb103..6fdbe3ef 100644 --- a/taiga/events/middleware.py +++ b/taiga/events/middleware.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 diff --git a/taiga/events/signal_handlers.py b/taiga/events/signal_handlers.py index 7f938f15..e50b0f4c 100644 --- a/taiga/events/signal_handlers.py +++ b/taiga/events/signal_handlers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,6 +27,8 @@ from . import events def on_save_any_model(sender, instance, created, **kwargs): # Ignore any object that can not have project_id + if not hasattr(instance, "project_id"): + return content_type = get_typename_for_model_instance(instance) # Ignore any other events diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 6fabb96d..e9da52c0 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 013e93e9..e09783a9 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py index 9728d01c..6126a04a 100644 --- a/taiga/export_import/management/commands/dump_project.py +++ b/taiga/export_import/management/commands/dump_project.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index 14016c6e..5afee1b8 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/mixins.py b/taiga/export_import/mixins.py index bc5504fa..89a625e6 100644 --- a/taiga/export_import/mixins.py +++ b/taiga/export_import/mixins.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/permissions.py b/taiga/export_import/permissions.py index 2f63d272..23516de9 100644 --- a/taiga/export_import/permissions.py +++ b/taiga/export_import/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/renderers.py b/taiga/export_import/renderers.py index 30bf8d5f..7f7a2a28 100644 --- a/taiga/export_import/renderers.py +++ b/taiga/export_import/renderers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index b0c78361..7b329e60 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -19,6 +19,7 @@ import copy import os from collections import OrderedDict +from django.apps import apps from django.core.files.base import ContentFile from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError @@ -43,6 +44,7 @@ from taiga.projects.attachments import models as attachments_models from taiga.timeline import models as timeline_models from taiga.timeline import service as timeline_service from taiga.users import models as users_models +from taiga.projects.notifications import services as notifications_services from taiga.projects.votes import services as votes_service from taiga.projects.history import services as history_service @@ -223,6 +225,48 @@ class HistoryDiffField(JsonField): return data +class WatcheableObjectModelSerializer(serializers.ModelSerializer): + watchers = UserRelatedField(many=True, required=False) + + def __init__(self, *args, **kwargs): + self._watchers_field = self.base_fields.pop("watchers", None) + super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs) + + """ + watchers is not a field from the model so we need to do some magic to make it work like a normal field + It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances + """ + + def restore_object(self, attrs, instance=None): + watcher_field = self.fields.pop("watchers", None) + instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance) + self._watchers = self.init_data.get("watchers", []) + return instance + + def save_watchers(self): + new_watcher_emails = set(self._watchers) + old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) + adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) + removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) + + User = apps.get_model("users", "User") + adding_users = User.objects.filter(email__in=adding_watcher_emails) + removing_users = User.objects.filter(email__in=removing_watcher_emails) + + for user in adding_users: + notifications_services.add_watcher(self.object, user) + + for user in removing_users: + notifications_services.remove_watcher(self.object, user) + + self.object.watchers = [user.email for user in self.object.get_watchers()] + + def to_native(self, obj): + ret = super(WatcheableObjectModelSerializer, self).to_native(obj) + ret["watchers"] = [user.email for user in obj.get_watchers()] + return ret + + class HistoryExportSerializer(serializers.ModelSerializer): user = HistoryUserField() diff = HistoryDiffField(required=False) @@ -243,7 +287,7 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer): def get_history(self, obj): history_qs = history_service.get_history_queryset_by_model_instance(obj, types=(history_models.HistoryType.change, history_models.HistoryType.create,)) - + return HistoryExportSerializer(history_qs, many=True).data @@ -447,9 +491,8 @@ class RolePointsExportSerializer(serializers.ModelSerializer): exclude = ('id', 'user_story') -class MilestoneExportSerializer(serializers.ModelSerializer): +class MilestoneExportSerializer(WatcheableObjectModelSerializer): owner = UserRelatedField(required=False) - watchers = UserRelatedField(many=True, required=False) modified_date = serializers.DateTimeField(required=False) estimated_start = serializers.DateField(required=False) estimated_finish = serializers.DateField(required=False) @@ -477,13 +520,12 @@ class MilestoneExportSerializer(serializers.ModelSerializer): class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, serializers.ModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") user_story = ProjectRelatedField(slug_field="ref", required=False) milestone = ProjectRelatedField(slug_field="name", required=False) assigned_to = UserRelatedField(required=False) - watchers = UserRelatedField(many=True, required=False) modified_date = serializers.DateTimeField(required=False) class Meta: @@ -495,13 +537,12 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, serializers.ModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): role_points = RolePointsExportSerializer(many=True, required=False) owner = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") milestone = ProjectRelatedField(slug_field="name", required=False) - watchers = UserRelatedField(many=True, required=False) modified_date = serializers.DateTimeField(required=False) generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) @@ -514,7 +555,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, serializers.ModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField(required=False) @@ -522,7 +563,6 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History severity = ProjectRelatedField(slug_field="name") type = ProjectRelatedField(slug_field="name") milestone = ProjectRelatedField(slug_field="name", required=False) - watchers = UserRelatedField(many=True, required=False) votes = serializers.SerializerMethodField("get_votes") modified_date = serializers.DateTimeField(required=False) @@ -538,10 +578,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - serializers.ModelSerializer): + WatcheableObjectModelSerializer): owner = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False) - watchers = UserRelatedField(many=True, required=False) modified_date = serializers.DateTimeField(required=False) class Meta: @@ -588,7 +627,7 @@ class TimelineExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project', 'namespace', 'object_id') -class ProjectExportSerializer(serializers.ModelSerializer): +class ProjectExportSerializer(WatcheableObjectModelSerializer): owner = UserRelatedField(required=False) default_points = serializers.SlugRelatedField(slug_field="name", required=False) default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index dee2c5c1..27d88d33 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -152,6 +152,7 @@ def store_project(data): if serialized.is_valid(): serialized.object._importing = True serialized.object.save() + serialized.save_watchers() return serialized add_errors("project", serialized.errors) return None @@ -298,6 +299,7 @@ def store_task(project, data): serialized.object._not_notify = True serialized.save() + serialized.save_watchers() if serialized.object.ref: sequence_name = refs.make_sequence_name(project) @@ -338,6 +340,7 @@ def store_milestone(project, milestone): serialized.object.project = project serialized.object._importing = True serialized.save() + serialized.save_watchers() for task_without_us in milestone.get("tasks_without_us", []): task_without_us["user_story"] = None @@ -358,7 +361,7 @@ def store_attachment(project, obj, attachment): serialized.object.owner = serialized.object.project.owner serialized.object._importing = True serialized.object.size = serialized.object.attached_file.size - serialized.object.name = path.basename(serialized.object.attached_file.name).lower() + serialized.object.name = path.basename(serialized.object.attached_file.name) serialized.save() return serialized add_errors("attachments", serialized.errors) @@ -401,6 +404,7 @@ def store_wiki_page(project, wiki_page): serialized.object._importing = True serialized.object._not_notify = True serialized.save() + serialized.save_watchers() for attachment in wiki_page.get("attachments", []): store_attachment(project, serialized.object, attachment) @@ -463,6 +467,7 @@ def store_user_story(project, data): serialized.object._not_notify = True serialized.save() + serialized.save_watchers() if serialized.object.ref: sequence_name = refs.make_sequence_name(project) @@ -523,6 +528,7 @@ def store_issue(project, data): serialized.object._not_notify = True serialized.save() + serialized.save_watchers() if serialized.object.ref: sequence_name = refs.make_sequence_name(project) diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index f833aef4..a33d2518 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -25,8 +25,7 @@ from django.utils import timezone from django.conf import settings from django.utils.translation import ugettext as _ -from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail - +from taiga.base.mails import mail_builder from taiga.celery import app from .service import render_project @@ -40,7 +39,6 @@ import resource @app.task(bind=True) def dump_project(self, user, project): - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id) storage_path = default_storage.path(path) @@ -56,7 +54,7 @@ def dump_project(self, user, project): "error_message": _("Error generating project dump"), "project": project } - email = mbuilder.export_error(user, ctx) + email = mail_builder.export_error(user, ctx) email.send() logger.error('Error generating dump %s (by %s)', project.slug, user, exc_info=sys.exc_info()) return @@ -68,7 +66,7 @@ def dump_project(self, user, project): "user": user, "deletion_date": deletion_date } - email = mbuilder.dump_project(user, ctx) + email = mail_builder.dump_project(user, ctx) email.send() @@ -79,8 +77,6 @@ def delete_project_dump(project_id, project_slug, task_id): @app.task def load_project_dump(user, dump): - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - try: project = dict_to_project(dump, user.email) except Exception: @@ -89,11 +85,11 @@ def load_project_dump(user, dump): "error_subject": _("Error loading project dump"), "error_message": _("Error loading project dump"), } - email = mbuilder.import_error(user, ctx) + email = mail_builder.import_error(user, ctx) email.send() logger.error('Error loading dump %s (by %s)', project.slug, user, exc_info=sys.exc_info()) return ctx = {"user": user, "project": project} - email = mbuilder.load_dump(user, ctx) + email = mail_builder.load_dump(user, ctx) email.send() diff --git a/taiga/export_import/throttling.py b/taiga/export_import/throttling.py index a59d7e33..8a772520 100644 --- a/taiga/export_import/throttling.py +++ b/taiga/export_import/throttling.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/external_apps/__init__.py b/taiga/external_apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/external_apps/admin.py b/taiga/external_apps/admin.py new file mode 100644 index 00000000..c1efa854 --- /dev/null +++ b/taiga/external_apps/admin.py @@ -0,0 +1,32 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.contrib import admin + +from . import models + + +class ApplicationAdmin(admin.ModelAdmin): + readonly_fields=("id",) + +admin.site.register(models.Application, ApplicationAdmin) + + +class ApplicationTokenAdmin(admin.ModelAdmin): + readonly_fields=("token",) + search_fields = ("user__username", "user__full_name", "user__email", "application__name") + +admin.site.register(models.ApplicationToken, ApplicationTokenAdmin) diff --git a/taiga/external_apps/api.py b/taiga/external_apps/api.py new file mode 100644 index 00000000..8da1ca9a --- /dev/null +++ b/taiga/external_apps/api.py @@ -0,0 +1,104 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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 . import serializers +from . import models +from . import permissions +from . import services + +from taiga.base import response +from taiga.base import exceptions as exc +from taiga.base.api import ModelCrudViewSet, ModelRetrieveViewSet +from taiga.base.api.utils import get_object_or_404 +from taiga.base.decorators import list_route, detail_route + +from django.db import transaction +from django.utils.translation import ugettext_lazy as _ + + +class Application(ModelRetrieveViewSet): + serializer_class = serializers.ApplicationSerializer + permission_classes = (permissions.ApplicationPermission,) + model = models.Application + + @detail_route(methods=["GET"]) + def token(self, request, *args, **kwargs): + if self.request.user.is_anonymous(): + raise exc.NotAuthenticated(_("Authentication required")) + + application = get_object_or_404(models.Application, **kwargs) + self.check_permissions(request, 'token', request.user) + try: + application_token = models.ApplicationToken.objects.get(user=request.user, application=application) + application_token.update_auth_code() + application_token.state = request.GET.get("state", None) + application_token.save() + + except models.ApplicationToken.DoesNotExist: + application_token = models.ApplicationToken( + user=request.user, + application=application + ) + + auth_code_data = serializers.ApplicationTokenSerializer(application_token).data + return response.Ok(auth_code_data) + + +class ApplicationToken(ModelCrudViewSet): + serializer_class = serializers.ApplicationTokenSerializer + permission_classes = (permissions.ApplicationTokenPermission,) + + def get_queryset(self): + if self.request.user.is_anonymous(): + raise exc.NotAuthenticated(_("Authentication required")) + + return models.ApplicationToken.objects.filter(user=self.request.user) + + @list_route(methods=["POST"]) + def authorize(self, request, pk=None): + if self.request.user.is_anonymous(): + raise exc.NotAuthenticated(_("Authentication required")) + + application_id = request.DATA.get("application", None) + state = request.DATA.get("state", None) + application_token = services.authorize_token(application_id, request.user, state) + + auth_code_data = serializers.AuthorizationCodeSerializer(application_token).data + return response.Ok(auth_code_data) + + @list_route(methods=["POST"]) + def validate(self, request, pk=None): + application_id = request.DATA.get("application", None) + auth_code = request.DATA.get("auth_code", None) + state = request.DATA.get("state", None) + application_token = get_object_or_404(models.ApplicationToken, + application__id=application_id, + auth_code=auth_code, + state=state) + + application_token.generate_token() + application_token.save() + + access_token_data = serializers.AccessTokenSerializer(application_token).data + return response.Ok(access_token_data) + + # POST method disabled + def create(self, *args, **kwargs): + raise exc.NotSupported() + + # PATCH and PUT methods disabled + def update(self, *args, **kwargs): + raise exc.NotSupported() diff --git a/taiga/external_apps/auth_backends.py b/taiga/external_apps/auth_backends.py new file mode 100644 index 00000000..6ab025bd --- /dev/null +++ b/taiga/external_apps/auth_backends.py @@ -0,0 +1,40 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import re + +from taiga.base.api.authentication import BaseAuthentication + +from . import services + +class Token(BaseAuthentication): + auth_rx = re.compile(r"^Application (.+)$") + + def authenticate(self, request): + if "HTTP_AUTHORIZATION" not in request.META: + return None + + token_rx_match = self.auth_rx.search(request.META["HTTP_AUTHORIZATION"]) + if not token_rx_match: + return None + + token = token_rx_match.group(1) + user = services.get_user_for_application_token(token) + + return (user, token) + + def authenticate_header(self, request): + return 'Bearer realm="api"' diff --git a/taiga/external_apps/encryption.py b/taiga/external_apps/encryption.py new file mode 100644 index 00000000..523c49d1 --- /dev/null +++ b/taiga/external_apps/encryption.py @@ -0,0 +1,30 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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 Crypto.PublicKey import RSA +from jwkest.jwk import SYMKey +from jwkest.jwe import JWE + + +def encrypt(content, key): + sym_key = SYMKey(key=key, alg="A128KW") + jwe = JWE(content, alg="A128KW", enc="A256GCM") + return jwe.encrypt([sym_key]) + + +def decrypt(content, key): + sym_key = SYMKey(key=key, alg="A128KW") + return JWE().decrypt(content, keys=[sym_key]) diff --git a/taiga/external_apps/migrations/0001_initial.py b/taiga/external_apps/migrations/0001_initial.py new file mode 100644 index 00000000..fbbc348b --- /dev/null +++ b/taiga/external_apps/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import taiga.external_apps.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.CharField(serialize=False, unique=True, max_length=255, default=taiga.external_apps.models._generate_uuid, primary_key=True)), + ('name', models.CharField(verbose_name='name', max_length=255)), + ('icon_url', models.TextField(null=True, blank=True, verbose_name='Icon url')), + ('web', models.CharField(null=True, blank=True, max_length=255, verbose_name='web')), + ('description', models.TextField(null=True, blank=True, verbose_name='description')), + ('next_url', models.TextField(verbose_name='Next url')), + ('key', models.TextField(verbose_name='secret key for ciphering the application tokens')), + ], + options={ + 'verbose_name_plural': 'applications', + 'verbose_name': 'application', + 'ordering': ['name'], + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ApplicationToken', + fields=[ + ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), + ('auth_code', models.CharField(null=True, blank=True, max_length=255, default=None)), + ('token', models.CharField(null=True, blank=True, max_length=255, default=None)), + ('state', models.CharField(null=True, blank=True, max_length=255, default='')), + ('application', models.ForeignKey(verbose_name='application', related_name='application_tokens', to='external_apps.Application')), + ('user', models.ForeignKey(verbose_name='user', related_name='application_tokens', to=settings.AUTH_USER_MODEL)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='applicationtoken', + unique_together=set([('application', 'user')]), + ), + ] diff --git a/taiga/external_apps/migrations/__init__.py b/taiga/external_apps/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/external_apps/models.py b/taiga/external_apps/models.py new file mode 100644 index 00000000..b1ffed26 --- /dev/null +++ b/taiga/external_apps/models.py @@ -0,0 +1,85 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from . import services + +import uuid + +def _generate_uuid(): + return str(uuid.uuid1()) + + +class Application(models.Model): + id = models.CharField(primary_key=True, max_length=255, unique=True, default=_generate_uuid) + + name = models.CharField(max_length=255, null=False, blank=False, + verbose_name=_("name")) + + icon_url = models.TextField(null=True, blank=True, verbose_name=_("Icon url")) + web = models.CharField(max_length=255, null=True, blank=True, verbose_name=_("web")) + description = models.TextField(null=True, blank=True, verbose_name=_("description")) + + next_url = models.TextField(null=False, blank=False, verbose_name=_("Next url")) + + key = models.TextField(null=False, blank=False, verbose_name=_("secret key for ciphering the application tokens")) + + class Meta: + verbose_name = "application" + verbose_name_plural = "applications" + ordering = ["name"] + + def __str__(self): + return self.name + + +class ApplicationToken(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, + related_name="application_tokens", + verbose_name=_("user")) + + application = models.ForeignKey("Application", blank=False, null=False, + related_name="application_tokens", + verbose_name=_("application")) + + auth_code = models.CharField(max_length=255, null=True, blank=True, default=None) + token = models.CharField(max_length=255, null=True, blank=True, default=None) + # An unguessable random string. It is used to protect against cross-site request forgery attacks. + state = models.CharField(max_length=255, null=True, blank=True, default="") + + class Meta: + unique_together = ("application", "user",) + + def __str__(self): + return "{application}: {user} - {token}".format(application=self.application.name, user=self.user.get_full_name(), token=self.token) + + @property + def cyphered_token(self): + return services.cypher_token(self) + + @property + def next_url(self): + return "{url}?auth_code={auth_code}".format(url=self.application.next_url, auth_code=self.auth_code) + + def update_auth_code(self): + self.auth_code = _generate_uuid() + + def generate_token(self): + self.auth_code = None + self.token = _generate_uuid() diff --git a/taiga/external_apps/permissions.py b/taiga/external_apps/permissions.py new file mode 100644 index 00000000..2132ea2a --- /dev/null +++ b/taiga/external_apps/permissions.py @@ -0,0 +1,43 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api.permissions import TaigaResourcePermission +from taiga.base.api.permissions import IsAuthenticated +from taiga.base.api.permissions import PermissionComponent + + +class ApplicationPermission(TaigaResourcePermission): + retrieve_perms = IsAuthenticated() + token_perms = IsAuthenticated() + list_perms = IsAuthenticated() + + +class CanUseToken(PermissionComponent): + def check_permissions(self, request, view, obj=None): + if not obj: + return False + + return request.user == obj.user + + +class ApplicationTokenPermission(TaigaResourcePermission): + retrieve_perms = IsAuthenticated() & CanUseToken() + by_application_perms = IsAuthenticated() + create_perms = IsAuthenticated() + update_perms = IsAuthenticated() & CanUseToken() + partial_update_perms = IsAuthenticated() & CanUseToken() + destroy_perms = IsAuthenticated() & CanUseToken() + list_perms = IsAuthenticated() diff --git a/taiga/external_apps/serializers.py b/taiga/external_apps/serializers.py new file mode 100644 index 00000000..bc3cc0fc --- /dev/null +++ b/taiga/external_apps/serializers.py @@ -0,0 +1,56 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import json + +from taiga.base.api import serializers + +from . import models +from . import services + +from django.utils.translation import ugettext as _ + + +class ApplicationSerializer(serializers.ModelSerializer): + class Meta: + model = models.Application + fields = ("id", "name", "web", "description", "icon_url") + + +class ApplicationTokenSerializer(serializers.ModelSerializer): + cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) + next_url = serializers.CharField(source="next_url", read_only=True) + application = ApplicationSerializer(read_only=True) + + class Meta: + model = models.ApplicationToken + fields = ("user", "id", "application", "auth_code", "next_url") + + +class AuthorizationCodeSerializer(serializers.ModelSerializer): + next_url = serializers.CharField(source="next_url", read_only=True) + class Meta: + model = models.ApplicationToken + fields = ("auth_code", "state", "next_url") + + +class AccessTokenSerializer(serializers.ModelSerializer): + cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) + next_url = serializers.CharField(source="next_url", read_only=True) + + class Meta: + model = models.ApplicationToken + fields = ("cyphered_token", ) diff --git a/taiga/external_apps/services.py b/taiga/external_apps/services.py new file mode 100644 index 00000000..7e1f78a4 --- /dev/null +++ b/taiga/external_apps/services.py @@ -0,0 +1,54 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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 exceptions as exc +from taiga.base.api.utils import get_object_or_404 + +from django.apps import apps +from django.utils.translation import ugettext as _ + +from . import encryption + +import json + +def get_user_for_application_token(token:str) -> object: + """ + Given an application token it tries to find an associated user + """ + app_token = apps.get_model("external_apps", "ApplicationToken").objects.filter(token=token).first() + if not app_token: + raise exc.NotAuthenticated(_("Invalid token")) + return app_token.user + + +def authorize_token(application_id:int, user:object, state:str) -> object: + ApplicationToken = apps.get_model("external_apps", "ApplicationToken") + Application = apps.get_model("external_apps", "Application") + application = get_object_or_404(Application, id=application_id) + token, _ = ApplicationToken.objects.get_or_create(user=user, application=application) + token.update_auth_code() + token.state = state + token.save() + return token + + +def cypher_token(application_token:object) -> str: + content = { + "token": application_token.token + } + + return encryption.encrypt(json.dumps(content), application_token.application.key) diff --git a/taiga/feedback/__init__.py b/taiga/feedback/__init__.py index 17e45261..69fa6d10 100644 --- a/taiga/feedback/__init__.py +++ b/taiga/feedback/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/admin.py b/taiga/feedback/admin.py index 512abb16..0c6f5e0c 100644 --- a/taiga/feedback/admin.py +++ b/taiga/feedback/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/api.py b/taiga/feedback/api.py index c0efb23d..46dc31bd 100644 --- a/taiga/feedback/api.py +++ b/taiga/feedback/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/apps.py b/taiga/feedback/apps.py index 7ae2c1af..8d8ec510 100644 --- a/taiga/feedback/apps.py +++ b/taiga/feedback/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/models.py b/taiga/feedback/models.py index a56de2b9..f60aee92 100644 --- a/taiga/feedback/models.py +++ b/taiga/feedback/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/permissions.py b/taiga/feedback/permissions.py index 6b755975..bbb53bdb 100644 --- a/taiga/feedback/permissions.py +++ b/taiga/feedback/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/routers.py b/taiga/feedback/routers.py index a3486b52..06d8988b 100644 --- a/taiga/feedback/routers.py +++ b/taiga/feedback/routers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/serializers.py b/taiga/feedback/serializers.py index 872bad90..647c0e96 100644 --- a/taiga/feedback/serializers.py +++ b/taiga/feedback/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/feedback/services.py b/taiga/feedback/services.py index e5f92c3c..6fee5c79 100644 --- a/taiga/feedback/services.py +++ b/taiga/feedback/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,7 +16,7 @@ from django.conf import settings -from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail +from taiga.base.mails import mail_builder def send_feedback(feedback_entry, extra, reply_to=[]): @@ -30,7 +30,6 @@ def send_feedback(feedback_entry, extra, reply_to=[]): "extra": extra } - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - email = mbuilder.feedback_notification(support_email, ctx) + email = mail_builder.feedback_notification(support_email, ctx) email.extra_headers["Reply-To"] = ", ".join(reply_to) email.send() diff --git a/taiga/front/sitemaps/__init__.py b/taiga/front/sitemaps/__init__.py index ba5da7bd..e07dd928 100644 --- a/taiga/front/sitemaps/__init__.py +++ b/taiga/front/sitemaps/__init__.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/base.py b/taiga/front/sitemaps/base.py index 83967f4d..418ed739 100644 --- a/taiga/front/sitemaps/base.py +++ b/taiga/front/sitemaps/base.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/generics.py b/taiga/front/sitemaps/generics.py index 27fbc075..41479a61 100644 --- a/taiga/front/sitemaps/generics.py +++ b/taiga/front/sitemaps/generics.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/issues.py b/taiga/front/sitemaps/issues.py index e4404138..912712fc 100644 --- a/taiga/front/sitemaps/issues.py +++ b/taiga/front/sitemaps/issues.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/milestones.py b/taiga/front/sitemaps/milestones.py index 7dde324b..049d3c9c 100644 --- a/taiga/front/sitemaps/milestones.py +++ b/taiga/front/sitemaps/milestones.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/projects.py b/taiga/front/sitemaps/projects.py index f9ad82f8..fc56adca 100644 --- a/taiga/front/sitemaps/projects.py +++ b/taiga/front/sitemaps/projects.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/tasks.py b/taiga/front/sitemaps/tasks.py index 264be4de..fa066a3b 100644 --- a/taiga/front/sitemaps/tasks.py +++ b/taiga/front/sitemaps/tasks.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/users.py b/taiga/front/sitemaps/users.py index c29420e0..0cd2c2ed 100644 --- a/taiga/front/sitemaps/users.py +++ b/taiga/front/sitemaps/users.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/userstories.py b/taiga/front/sitemaps/userstories.py index da16d19b..669db7ed 100644 --- a/taiga/front/sitemaps/userstories.py +++ b/taiga/front/sitemaps/userstories.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/sitemaps/wiki.py b/taiga/front/sitemaps/wiki.py index eeb96a58..33c0cf0e 100644 --- a/taiga/front/sitemaps/wiki.py +++ b/taiga/front/sitemaps/wiki.py @@ -1,5 +1,5 @@ -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/front/templatetags/functions.py b/taiga/front/templatetags/functions.py index 9a510d50..1c2fdaea 100644 --- a/taiga/front/templatetags/functions.py +++ b/taiga/front/templatetags/functions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -21,10 +21,7 @@ from django_sites import get_by_id as get_site_by_id from taiga.front.urls import urls -register = library.Library() - - -@register.global_function(name="resolve_front_url") +@library.global_function(name="resolve_front_url") def resolve(type, *args): site = get_site_by_id("front") url_tmpl = "{scheme}//{domain}{url}" diff --git a/taiga/front/urls.py b/taiga/front/urls.py index b377f655..5987e5af 100644 --- a/taiga/front/urls.py +++ b/taiga/front/urls.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/api.py b/taiga/hooks/api.py index d4345cbc..d807f19d 100644 --- a/taiga/hooks/api.py +++ b/taiga/hooks/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py index 0b304ac8..781eca96 100644 --- a/taiga/hooks/bitbucket/api.py +++ b/taiga/hooks/bitbucket/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py index 96e6c29f..3ee7c92b 100644 --- a/taiga/hooks/bitbucket/event_hooks.py +++ b/taiga/hooks/bitbucket/event_hooks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -141,8 +141,8 @@ class IssuesEventHook(BaseEventHook): if number and subject and bitbucket_user_name and bitbucket_user_url: comment = _("Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} " "\"See @{bitbucket_user_name}'s BitBucket profile\") " - "from BitBucket.\nOrigin BitBucket issue: [gh#{number} - {subject}]({bitbucket_url} " - "\"Go to 'gh#{number} - {subject}'\"):\n\n" + "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} " + "\"Go to 'bb#{number} - {subject}'\"):\n\n" "{description}").format(bitbucket_user_name=bitbucket_user_name, bitbucket_user_url=bitbucket_user_url, number=number, @@ -184,8 +184,8 @@ class IssueCommentEventHook(BaseEventHook): if number and subject and bitbucket_user_name and bitbucket_user_url: comment = _("Comment by [@{bitbucket_user_name}]({bitbucket_user_url} " "\"See @{bitbucket_user_name}'s BitBucket profile\") " - "from BitBucket.\nOrigin BitBucket issue: [gh#{number} - {subject}]({bitbucket_url} " - "\"Go to 'gh#{number} - {subject}'\")\n\n" + "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} " + "\"Go to 'bb#{number} - {subject}'\")\n\n" "{message}").format(bitbucket_user_name=bitbucket_user_name, bitbucket_user_url=bitbucket_user_url, number=number, diff --git a/taiga/hooks/bitbucket/services.py b/taiga/hooks/bitbucket/services.py index ddd4af79..ff39d083 100644 --- a/taiga/hooks/bitbucket/services.py +++ b/taiga/hooks/bitbucket/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,6 +23,7 @@ from taiga.users.models import User from taiga.base.utils.urls import get_absolute_url +# Set this in settings.PROJECT_MODULES_CONFIGURATORS["bitbucket"] def get_or_generate_config(project): config = project.modules_config.config if config and "bitbucket" in config: diff --git a/taiga/hooks/event_hooks.py b/taiga/hooks/event_hooks.py index 0d26be38..eebc45a0 100644 --- a/taiga/hooks/event_hooks.py +++ b/taiga/hooks/event_hooks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/exceptions.py b/taiga/hooks/exceptions.py index 697674d4..1a214ab0 100644 --- a/taiga/hooks/exceptions.py +++ b/taiga/hooks/exceptions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/github/api.py b/taiga/hooks/github/api.py index 8251858f..9082a2e6 100644 --- a/taiga/hooks/github/api.py +++ b/taiga/hooks/github/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py index 3dbd0417..20bbf11c 100644 --- a/taiga/hooks/github/event_hooks.py +++ b/taiga/hooks/github/event_hooks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py index bc16c380..31a4f063 100644 --- a/taiga/hooks/github/services.py +++ b/taiga/hooks/github/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,6 +23,7 @@ from taiga.users.models import AuthData from taiga.base.utils.urls import get_absolute_url +# Set this in settings.PROJECT_MODULES_CONFIGURATORS["github"] def get_or_generate_config(project): config = project.modules_config.config if config and "github" in config: diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py index 48d70fe7..4cc71fd9 100644 --- a/taiga/hooks/gitlab/api.py +++ b/taiga/hooks/gitlab/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -30,6 +30,7 @@ class GitLabViewSet(BaseWebhookApiViewSet): event_hook_classes = { "push": event_hooks.PushEventHook, "issue": event_hooks.IssuesEventHook, + "note": event_hooks.IssueCommentEventHook, } def _validate_signature(self, project, request): diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py index 84079121..4c1f7dd3 100644 --- a/taiga/hooks/gitlab/event_hooks.py +++ b/taiga/hooks/gitlab/event_hooks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -89,7 +89,7 @@ class PushEventHook(BaseEventHook): def replace_gitlab_references(project_url, wiki_text): - if wiki_text == None: + if wiki_text is None: wiki_text = "" template = "\g<1>[GitLab#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) @@ -127,3 +127,48 @@ class IssuesEventHook(BaseEventHook): snapshot = take_snapshot(issue, comment=_("Created from GitLab"), user=get_gitlab_user(None)) send_notifications(issue, history=snapshot) + + +class IssueCommentEventHook(BaseEventHook): + def process_event(self): + if self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue": + return + + number = self.payload.get('issue', {}).get('iid', None) + subject = self.payload.get('issue', {}).get('title', None) + + project_url = self.payload.get('repository', {}).get('homepage', None) + + gitlab_url = os.path.join(project_url, "issues", str(number)) + gitlab_user_name = self.payload.get('user', {}).get('username', None) + gitlab_user_url = os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", gitlab_user_name) + + comment_message = self.payload.get('object_attributes', {}).get('note', None) + comment_message = replace_gitlab_references(project_url, comment_message) + + user = get_gitlab_user(None) + + if not all([comment_message, gitlab_url, project_url]): + raise ActionSyntaxException(_("Invalid issue comment information")) + + issues = Issue.objects.filter(external_reference=["gitlab", gitlab_url]) + tasks = Task.objects.filter(external_reference=["gitlab", gitlab_url]) + uss = UserStory.objects.filter(external_reference=["gitlab", gitlab_url]) + + for item in list(issues) + list(tasks) + list(uss): + if number and subject and gitlab_user_name and gitlab_user_url: + comment = _("Comment by [@{gitlab_user_name}]({gitlab_user_url} " + "\"See @{gitlab_user_name}'s GitLab profile\") " + "from GitLab.\nOrigin GitLab issue: [gl#{number} - {subject}]({gitlab_url} " + "\"Go to 'gl#{number} - {subject}'\")\n\n" + "{message}").format(gitlab_user_name=gitlab_user_name, + gitlab_user_url=gitlab_user_url, + number=number, + subject=subject, + gitlab_url=gitlab_url, + message=comment_message) + else: + comment = _("Comment From GitLab:\n\n{message}").format(message=comment_message) + + snapshot = take_snapshot(item, comment=comment, user=user) + send_notifications(item, history=snapshot) diff --git a/taiga/hooks/gitlab/migrations/0001_initial.py b/taiga/hooks/gitlab/migrations/0001_initial.py index 8002d965..4698c158 100644 --- a/taiga/hooks/gitlab/migrations/0001_initial.py +++ b/taiga/hooks/gitlab/migrations/0001_initial.py @@ -29,7 +29,7 @@ def create_github_system_user(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('users', '0006_auto_20141030_1132') + ('users', '0011_user_theme') ] operations = [ diff --git a/taiga/hooks/gitlab/migrations/0002_auto_20150703_1102.py b/taiga/hooks/gitlab/migrations/0002_auto_20150703_1102.py new file mode 100644 index 00000000..4613c4ac --- /dev/null +++ b/taiga/hooks/gitlab/migrations/0002_auto_20150703_1102.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.core.files import File + +def update_gitlab_system_user_photo_to_v2(apps, schema_editor): + # We get the model from the versioned app registry; + # if we directly import it, it'll be the wrong version + User = apps.get_model("users", "User") + db_alias = schema_editor.connection.alias + + try: + user = User.objects.using(db_alias).get(username__startswith="gitlab-", + is_active=False, + is_system=True) + f = open("taiga/hooks/gitlab/migrations/logo-v2.png", "rb") + user.photo.save("logo.png", File(f)) + user.save() + except User.DoesNotExist: + pass + +def update_gitlab_system_user_photo_to_v1(apps, schema_editor): + # We get the model from the versioned app registry; + # if we directly import it, it'll be the wrong version + User = apps.get_model("users", "User") + db_alias = schema_editor.connection.alias + + try: + user = User.objects.using(db_alias).get(username__startswith="gitlab-", + is_active=False, + is_system=True) + f = open("taiga/hooks/gitlab/migrations/logo.png", "rb") + user.photo.save("logo.png", File(f)) + user.save() + except User.DoesNotExist: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('gitlab', '0001_initial'), + ('users', '0011_user_theme'), + ] + + operations = [ + migrations.RunPython(update_gitlab_system_user_photo_to_v2, + update_gitlab_system_user_photo_to_v1), + ] diff --git a/taiga/hooks/gitlab/migrations/logo-v2.png b/taiga/hooks/gitlab/migrations/logo-v2.png new file mode 100644 index 00000000..01063fc3 Binary files /dev/null and b/taiga/hooks/gitlab/migrations/logo-v2.png differ diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py index 2d99969a..a441c0dc 100644 --- a/taiga/hooks/gitlab/services.py +++ b/taiga/hooks/gitlab/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,6 +23,7 @@ from taiga.users.models import User from taiga.base.utils.urls import get_absolute_url +# Set this in settings.PROJECT_MODULES_CONFIGURATORS["gitlab"] def get_or_generate_config(project): config = project.modules_config.config if config and "gitlab" in config: diff --git a/taiga/locale/api.py b/taiga/locale/api.py index 9a35be0b..03f11d56 100644 --- a/taiga/locale/api.py +++ b/taiga/locale/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po index 956c3a16..7ee77e1b 100644 --- a/taiga/locale/ca/LC_MESSAGES/django.po +++ b/taiga/locale/ca/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: @@ -9,10 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-09 07:47+0000\n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" "Last-Translator: Taiga Dev Team \n" -"Language-Team: Catalan (http://www.transifex.com/projects/p/taiga-back/" +"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ca/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -32,40 +32,41 @@ msgstr "Sistema de registre invàlid" msgid "invalid login type" msgstr "Sistema de login invàlid" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "nom d'usuari invàlid" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Requerit. 255 caràcters o menys. Lletres, nombres i caràcters /./-/_" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "El mot d'usuari ja està en ús." -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "Aquest e-mail ja està en ús." -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "El token no s'ajusta a cap invitació vàlida" -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "Aquest usuari ja està registrat" -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "Aquest usuari ja es membre" -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "Error creant un nou usuari." #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "Token invàlid" @@ -329,12 +330,12 @@ msgstr "Error d'integritat per argument invàlid o erroni." msgid "Precondition error" msgstr "Precondició errònia." -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "" @@ -521,32 +522,32 @@ msgstr "" msgid "error importing timelines" msgstr "" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "Conté camps personalitzats invàlids." -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "" @@ -697,11 +698,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] El teu bolcat de dades ha sigut importat" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "Nom" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "Descripció" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "Nom complet" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "Adreça d'email" @@ -709,12 +760,14 @@ msgstr "Adreça d'email" msgid "comment" msgstr "Comentari" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "Data de creació" @@ -783,7 +836,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "El payload no és un arxiu json vàlid" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "El projecte no existeix" @@ -791,29 +845,66 @@ msgstr "El projecte no existeix" msgid "Bad signature" msgstr "Firma no vàlida." -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "El payload no és un application/x-www-form-urlencoded vàlid" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "El payload no és vàlid" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "L'element referenciat no existeix" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "L'estatus no existeix." -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Informació d'incidència no vàlida." + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Informació del comentari a l'incidència no vàlid." + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -821,15 +912,11 @@ msgid "" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." msgstr "" -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "" -#: 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/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -840,15 +927,11 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "" -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "Informació del comentari a l'incidència no vàlid." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -859,7 +942,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -867,21 +950,40 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "Veure projecte" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "Veure fita" @@ -889,240 +991,232 @@ msgstr "Veure fita" msgid "View user stories" msgstr "Veure història d'usuari" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "Veure tasca" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "Veure incidència" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "Veure pàgina del wiki" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "Veure links del wiki" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "Vota incidéncies" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "Demana membresía" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "Afegeix història d'usuari a projecte" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "Afegeix comentaris a històries d'usuari" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "Afegeix comentaris a tasques" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "Afegeix incidéncies" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "Afegeix comentaris a incidéncies" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "Afegeix pàgina del wiki" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "Modifica pàgina del wiki" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "Afegeix enllaç de wiki" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "Modifica enllaç de wiki" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "Afegeix fita" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "Modifica fita" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "Borra fita" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "Veure història d'usuari" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "Afegeix història d'usuari" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "Modifica història d'usuari" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "Borra història d'usuari" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "Afegeix tasca" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "Modifica tasca" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "Borra tasca" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "Afegeix incidència" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "Modifica incidència" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "Borra incidència" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "Borra pàgina de wiki" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "Borra enllaç de wiki" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "Modifica projecte" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "Afegeix membre" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "Borra membre" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "Borra projecte" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "Administrar valors de projecte" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "Administrar rols" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 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:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "No tens permisos per a veure açò." #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" +msgid "Partial updates are not supported" msgstr "" #: taiga/projects/attachments/api.py:62 msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "Amo" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "Projecte" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "Tipus de contingut" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "Id d'objecte" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "Data de modificació" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "Arxiu adjunt" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "està obsolet " #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "Descripció" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "Ordre" @@ -1135,33 +1229,44 @@ msgid "Jitsi" msgstr "" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "Nom" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "tipus" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "història d'usuari" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "tasca" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "incidéncia" @@ -1241,7 +1346,7 @@ msgstr "Borrat" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "Sense assignar" @@ -1288,33 +1393,37 @@ msgstr "Desde:" msgid "To:" msgstr "A:" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "contingut" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "nota de bloqueig" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "" + +#: taiga/projects/issues/api.py:160 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:143 +#: taiga/projects/issues/api.py:164 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:147 +#: taiga/projects/issues/api.py:168 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:151 +#: taiga/projects/issues/api.py:172 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:155 +#: taiga/projects/issues/api.py:176 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" @@ -1336,10 +1445,6 @@ msgstr "severitat" msgid "priority" msgstr "prioritat" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "tipus" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1364,10 +1469,23 @@ msgstr "assignada a" msgid "external reference" msgstr "referència externa" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "slug" @@ -1379,8 +1497,8 @@ msgstr "Data estimada d'inici" msgid "estimated finish date" msgstr "Data estimada de finalització" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "està tancat" @@ -1409,215 +1527,220 @@ msgstr "" msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "email" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "token" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "text extra d'invitació" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "L'usuari ja es membre del projecte" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "Points per defecte" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "estatus d'història d'usuai per defecte" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "Estatus de tasca per defecte" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "Prioritat per defecte" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "Severitat per defecte" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "Status d'incidència per defecte" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "Tipus d'incidència per defecte" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "membres" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "total de fites" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "total de punts d'història" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "activa panell de backlog" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "activa panell de kanban" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "activa panell de wiki" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "activa panell d'incidències" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "sistema de videoconferència" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" -msgstr "sala videoconferència" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "template de creació" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "permisos d'anònims" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "permisos d'usuaris" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "es privat" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "colors de tags" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "configuració de mòdules" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "està arxivat" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "color" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "limit de treball en progrés" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "valor" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "rol d'amo per defecte" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "opcions per defecte" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "status d'històries d'usuari" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "punts" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "status de tasques" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "status d'incidències" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "tipus d'incidències" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "prioritats" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "severitats" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "rols" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "No observant" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "Observant" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "Ignorant" +msgid "None" +msgstr "" -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "Observadors" - -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "created date time" msgstr "creada data" -#: taiga/projects/notifications/models.py:61 +#: taiga/projects/notifications/models.py:63 msgid "updated date time" msgstr "Actualitzada data" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2095,7 +2218,7 @@ msgstr "" "\n" "[%(project)s] Borrada pàgina de Wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "" @@ -2119,66 +2242,69 @@ msgstr "Versió" 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" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "Aquest e-mail ja està en ús" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "Rol invàlid per al projecte" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "" - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "Opcions per defecte" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "Estatus d'històries d'usuari" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "Punts" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "Estatus de tasques" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "Estatus d'incidéncies" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "Tipus d'incidéncies" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "Prioritats" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "Severitats" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "Rols" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." -msgstr "No tens permissos per a afegir/modificar aquesta tasca." +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." +msgstr "" #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -2554,14 +2680,18 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:174 -#, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" msgstr "" -"Generant l'història d'usuari [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -2609,34 +2739,34 @@ msgid "There's no task status with that id" msgstr "No hi ha cap estatus de tasca amb eixe id" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "Vots" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "vots" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "Vot" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "últim a modificar" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "Informació personal" @@ -2649,65 +2779,65 @@ msgstr "Permissos" msgid "Important dates" msgstr "Dates importants" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "Nom d'usuari o email invàlid" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "Correu enviat satisfactòriament" - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "Token invàlid" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "Paràmetre de password actual requerit" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "Paràmetre de password requerit" - -#: taiga/users/api.py:184 -msgid "Invalid password length at least 6 charaters needed" -msgstr "Password invàlid, al menys 6 caràcters requerits" - -#: taiga/users/api.py:187 -msgid "Invalid current password" -msgstr "Password actual invàlid" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "Arguments incomplets." - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "Format d'image invàlid" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "Email duplicat" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "Email no vàlid" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Nom d'usuari o email invàlid" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "Correu enviat satisfactòriament" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Token invàlid" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Paràmetre de password actual requerit" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Paràmetre de password requerit" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Password invàlid, al menys 6 caràcters requerits" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "Password actual invàlid" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Arguments incomplets." + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Format d'image invàlid" + +#: taiga/users/api.py:256 taiga/users/api.py:262 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:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "Invàlid. Estás segur que el token es correcte?" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "estatus de superusuari" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -2715,24 +2845,24 @@ msgstr "" "Designa que aquest usuari te tots els permisos sense asignarli-los " "explícitament." -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "mot d'usuari" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 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:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "Introdueix un nom d'usuari vàlid" -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "actiu" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2740,55 +2870,55 @@ msgstr "" "Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en " "lloc de borrar el compte." -#: taiga/users/models.py:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "foto" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "data d'unió" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "llenguatge per defecte" -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "zona horaria per defecte" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "coloritza tags" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "token de correu" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "nova adreça de correu" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "permissos" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "invàlid" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "Nom d'usuari invàlid" -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "" diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po index 12fedc19..8ce02f38 100644 --- a/taiga/locale/de/LC_MESSAGES/django.po +++ b/taiga/locale/de/LC_MESSAGES/django.po @@ -1,22 +1,25 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: # Chris , 2015 +# Christoph Harrer, 2015 # Hans Raaf, 2015 # Hans Raaf, 2015 +# Henning Matthaei, 2015 # Regina , 2015 # Sebastian Blum , 2015 +# Silsha Fux , 2015 # Thomas McWork , 2015 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-29 10:23+0000\n" -"Last-Translator: Regina \n" -"Language-Team: German (http://www.transifex.com/projects/p/taiga-back/" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -36,42 +39,43 @@ msgstr "Ungültige Registrierungsart" msgid "invalid login type" msgstr "Ungültige Loginart" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "Ungültiger Benutzername" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "255 oder weniger Zeichen aus Buchstaben, Zahlen und Punkt, Minus oder " "Unterstrich erforderlich." -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "Der Benutzername wird schon verwendet." -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "Diese E-Mail Adresse wird schon verwendet." -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden." -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "Der Benutzer ist schon registriert." -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "Der Benutzer für diese Mitgliedschaft existiert bereits." -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "Fehler bei der Erstellung des neuen Benutzers." #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "Ungültiges Token" @@ -196,6 +200,8 @@ msgstr "" #: taiga/base/api/fields.py:957 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" +"Bitte senden Sie entweder eine Datei oder markieren Sie \"Löschen\", nicht " +"beides." #: taiga/base/api/fields.py:997 msgid "" @@ -355,12 +361,12 @@ msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente" msgid "Precondition error" msgstr "Voraussetzungsfehler" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "Fehler in Filter Parameter Typen." -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "'project' muss ein Integer-Wert sein." @@ -570,32 +576,32 @@ msgstr "Fehler beim Importieren der Schlagworte" msgid "error importing timelines" msgstr "Fehler beim Importieren der Chroniken" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "Enthält ungültige Benutzerfelder." -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "Der Name für das Projekt ist doppelt vergeben" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "Fehler beim Erzeugen der Projekt Export-Datei " -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "Fehler beim Laden von Projekt Export-Datei" @@ -839,11 +845,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Ihre Projekt Export-Datei wurde importiert" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "Name" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "Icon URL" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "Web" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "Beschreibung" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "Nächste URL" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "Benutzer" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "Applikation" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "vollständiger Name" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "E-Mail Adresse" @@ -851,12 +907,14 @@ msgstr "E-Mail Adresse" msgid "comment" msgstr "Kommentar" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "Erstellungsdatum" @@ -925,7 +983,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Die Nutzlast ist kein gültiges json" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "Das Projekt existiert nicht" @@ -933,29 +992,69 @@ msgstr "Das Projekt existiert nicht" msgid "Bad signature" msgstr "Falsche Signatur" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "Die Nutzlast ist eine ungültige Anwendung/x-www-form-urlencoded" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "Die Nutzlast ist ungültig" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "Das referenzierte Element existiert nicht" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "Der Status existiert nicht" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "Der Status des BitBucket Commits hat sich geändert" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Ungültige Ticket-Information" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "Ticket erstellt von BitBucket." + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Ungültige Ticket-Kommentar Information" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"Kommentar von BitBucket\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -966,15 +1065,11 @@ msgstr "" "@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "Der Status des GitHub Commits hat sich geändert" -#: 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/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -991,15 +1086,11 @@ msgstr "" "\n" " {description}" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "Ticket erstellt von GitHub." -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "Ungültige Ticket-Kommentar Information" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -1016,7 +1107,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -1027,21 +1118,43 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "Der Status des GitLab Commits hat sich geändert" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "Erstellt von GitLab" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"Kommentar von GitLab:\n" +"\n" +"{message}" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "Projekt ansehen" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "Meilensteine ansehen" @@ -1049,240 +1162,232 @@ msgstr "Meilensteine ansehen" msgid "View user stories" msgstr "User-Stories ansehen. " -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "Aufgaben ansehen" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "Tickets ansehen" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "Wiki Seiten ansehen" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "Wiki Links ansehen" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "Tickets auswählen" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "Mitgliedschaft beantragen" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "User-Story zu Projekt hinzufügen" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "Kommentar zu User-Stories hinzufügen" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "Kommentare zu Aufgaben hinzufügen" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "Tickets hinzufügen" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "Kommentare zu Tickets hinzufügen" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "Wiki Seite hinzufügen" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "Wiki Seite ändern" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "Wiki Link hinzufügen" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "Wiki Link ändern" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "Meilenstein hinzufügen" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "Meilenstein ändern" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "Meilenstein löschen" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "User-Story ansehen" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "User-Story hinzufügen" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "User-Story ändern" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "User-Story löschen" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "Aufgabe hinzufügen" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "Aufgabe ändern" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "Aufgabe löschen" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "Ticket hinzufügen" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "Ticket ändern" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "Gelöschtes Ticket" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "Wiki Seite löschen" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "Wiki Link löschen" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "Projekt ändern" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "Mitglied hinzufügen" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "Mitglied entfernen" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "Projekt löschen" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "Administrator Projekt Werte" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "Administrator-Rollen" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "Unglültiger Templatename" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "Ungültige Templatebeschreibung" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 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:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "Sie haben keine Berechtigungen für diese Ansicht" #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" -msgstr "Es werden nur Teilaktualisierungen unterstützt" +msgid "Partial updates are not supported" +msgstr "" #: taiga/projects/attachments/api.py:62 msgid "Project ID not matches between object and project" msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt" -#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "Besitzer" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "Projekt" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "Inhaltsart" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "Objekt Nr." -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "Zeitpunkt der Änderung" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "Angehangene Datei" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "SHA1" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "wurde verworfen" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "Beschreibung" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "Reihenfolge" @@ -1295,33 +1400,44 @@ msgid "Jitsi" msgstr "Jitsi" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "Kunde" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "Gesprächig" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "Name" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Text" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Mehrzeiliger Text" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "Datum" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "Art" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "Werte" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "User-Story" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "Aufgabe" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "Ticket" @@ -1401,7 +1517,7 @@ msgstr "entfernt" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "Nicht zugewiesen" @@ -1448,37 +1564,41 @@ msgstr "Von:" msgid "To:" msgstr "An:" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "Inhalt" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "Blockierungsgrund" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "Sprint" + +#: taiga/projects/issues/api.py:160 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:143 +#: taiga/projects/issues/api.py:164 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:147 +#: taiga/projects/issues/api.py:168 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:151 +#: taiga/projects/issues/api.py:172 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:155 +#: taiga/projects/issues/api.py:176 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." @@ -1500,10 +1620,6 @@ msgstr "Gewichtung" msgid "priority" msgstr "Priorität" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "Art" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1528,10 +1644,23 @@ msgstr "zugewiesen an" msgid "external reference" msgstr "externe Referenz" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "Count" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "Likes" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "Like" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "Slug" @@ -1543,8 +1672,8 @@ msgstr "geschätzter Starttermin" msgid "estimated finish date" msgstr "geschätzter Endtermin" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "ist geschlossen" @@ -1573,215 +1702,220 @@ msgstr "'{param}' Parameter ist ein Pflichtfeld" msgid "'project' parameter is mandatory" msgstr "Der 'project' Parameter ist ein Pflichtfeld" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "E-Mail" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "erstellt am " -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "Token" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "Einladung Zusatztext " -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "Benutzerreihenfolge" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "Der Benutzer ist bereits Mitglied dieses Projekts" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "voreingestellte Punkte" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "voreingesteller User-Story Status " -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "voreingestellter Aufgabenstatus" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "voreingestellte Priorität " -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "voreingestellte Gewichtung " -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "voreingestellter Ticket Status" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "voreingestellter Ticket Typ" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "Mitglieder" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "Meilensteine Gesamt" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "Story Punkte insgesamt" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "aktives Backlog Panel" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "aktives Kanban Panel" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "aktives Wiki Panel" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "aktives Tickets Panel" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "Videokonferenzsystem" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "Rechte für anonyme Nutzer" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "Rechte für registrierte Nutzer" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "ist privat" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "Tag Farben" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "Module konfigurieren" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "ist archiviert" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "Farbe" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "Ausführungslimit" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "Wert" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "voreingestellte Besitzerrolle" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "Vorgabe Optionen" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "User-Story Status " -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "Punkte" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "Aufgaben Status" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "Ticket Status" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "Ticket Arten" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "Prioritäten" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "Gewichtung" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "Rollen" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "Nicht beobachten" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "Beobachten" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "Ignorieren" - -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "Beobachter" - -#: taiga/projects/notifications/models.py:59 -msgid "created date time" +msgid "None" msgstr "" #: taiga/projects/notifications/models.py:61 -msgid "updated date time" +msgid "created date time" msgstr "" #: taiga/projects/notifications/models.py:63 +msgid "updated date time" +msgstr "" + +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "Chronik Einträge" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "Benutzer benachrichtigen" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "Beobachtet" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2537,7 +2671,7 @@ msgstr "" "[%(project)s] löschte die Wiki Seite \"%(page)s\"\n" "\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "Beobachter enthält ungültige Benutzer " @@ -2563,67 +2697,73 @@ msgstr "" "Sie können das Projekt nicht verlassen, wenn keine weiteren Besitzer " "vorhanden sind" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "Die E-Mailadresse ist bereits vergeben" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "Ungültige Rolle für dieses Projekt" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "Die Gesamtzahl der Meilensteine muss größer oder gleich Null sein " - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "Voreingestellte Optionen" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "Status für User-Stories" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "Punkte" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "Aufgaben Status" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "Ticket Status" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "Ticket Arten" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "Prioritäten" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "Gewichtung" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "Rollen" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "Zukünftiger Sprint" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "Projektende" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." msgstr "" -"Sie haben nicht die Berechtigungen, um diese Aufgabe hinzuzufügen/zu ändern." +"Sie haben nicht die Berechtigung, diesen Sprint auf diese Aufgabe zu setzen" + +#: taiga/projects/tasks/api.py:107 +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:110 +msgid "You don't have permissions to set this status to this task." +msgstr "" +"Sie haben nicht die Berechtigung, diesen Status auf diese Aufgabe zu setzen." #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -3016,14 +3156,22 @@ msgstr "Projekteigentümer " msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:174 -#, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +#: taiga/projects/userstories/api.py:156 +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:160 +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:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" msgstr "" -"Erstelle die User-Story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -3071,34 +3219,34 @@ msgid "There's no task status with that id" msgstr "Es gibt keinen Aufgabenstatus mit dieser id" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "Stimmen" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "Stimmen" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "Stimme" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "'content' Parameter ist erforderlich" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "'project_id' Parameter ist erforderlich" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "letzte Änderung" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "Personal Information" @@ -3111,66 +3259,66 @@ msgstr "Berechtigungen" msgid "Important dates" msgstr "Wichtige Termine" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "Ungültiger Benutzername oder E-Mail" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "E-Mail erfolgreich gesendet." - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "Token ist ungültig" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "Aktueller Passwort Parameter wird benötigt" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "Neuer Passwort Parameter benötigt" - -#: taiga/users/api.py:184 -msgid "Invalid password length at least 6 charaters needed" -msgstr "Ungültige Passwortlänge, mindestens 6 Zeichen erforderlich" - -#: taiga/users/api.py:187 -msgid "Invalid current password" -msgstr "Ungültiges aktuelles Passwort" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "Unvollständige Argumente" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "Ungültiges Bildformat" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "Doppelte E-Mail" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "Ungültige E-Mail" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Ungültiger Benutzername oder E-Mail" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "E-Mail erfolgreich gesendet." + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Token ist ungültig" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Aktueller Passwort Parameter wird benötigt" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Neuer Passwort Parameter benötigt" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Ungültige Passwortlänge, mindestens 6 Zeichen erforderlich" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "Ungültiges aktuelles Passwort" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Unvollständige Argumente" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Ungültiges Bildformat" + +#: taiga/users/api.py:256 taiga/users/api.py:262 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:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "Ungültig. Sind Sie sicher, dass das Token korrekt ist?" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "Superuser Status" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3178,25 +3326,25 @@ msgstr "" "Dieser Benutzer soll alle Berechtigungen erhalten, ohne dass diese zuvor " "zugewiesen werden müssen. " -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "Benutzername" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 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:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "Geben Sie einen gültigen Benuzternamen ein." -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "aktiv" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3204,55 +3352,55 @@ msgstr "" "Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen " "Benutzer zu löschen." -#: taiga/users/models.py:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "Über mich" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "Foto" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "Beitrittsdatum" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "Vorgegebene Sprache" -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "Standard-Theme" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "Vorgegebene Zeitzone" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "Tag-Farben" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "E-Mail Token" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "neue E-Mail Adresse" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "Berechtigungen" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "ungültig" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen." -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein." @@ -3415,6 +3563,8 @@ msgid "" "\n" "You may remove your account from this service: %(url)s\n" msgstr "" +"\n" +"Sie können Ihren Account von diesem Dienst trennen: %(url)s\n" #: taiga/users/templates/emails/registered_user-subject.jinja:1 msgid "You've been Taigatized!" diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index ce6bab28..f9ac0a78 100644 --- a/taiga/locale/en/LC_MESSAGES/django.po +++ b/taiga/locale/en/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # FIRST AUTHOR , YEAR. # @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -28,40 +28,41 @@ msgstr "" msgid "invalid login type" msgstr "" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "" -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "" -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "" -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "" -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "" -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "" #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "" @@ -321,12 +322,12 @@ msgstr "" msgid "Precondition error" msgstr "" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "" @@ -510,32 +511,32 @@ msgstr "" msgid "error importing timelines" msgstr "" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "" -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "" @@ -686,11 +687,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "" @@ -698,12 +749,14 @@ msgstr "" msgid "comment" msgstr "" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "" @@ -756,7 +809,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "" @@ -764,29 +818,66 @@ msgstr "" msgid "Bad signature" msgstr "" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -794,15 +885,11 @@ msgid "" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." msgstr "" -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "" -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -813,15 +900,11 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "" -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -832,7 +915,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -840,21 +923,40 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "" @@ -862,240 +964,232 @@ msgstr "" msgid "View user stories" msgstr "" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 msgid "At least one of the user must be an active admin" msgstr "" -#: taiga/projects/api.py:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "" #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" +msgid "Partial updates are not supported" msgstr "" #: taiga/projects/attachments/api.py:62 msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "" @@ -1108,33 +1202,44 @@ msgid "Jitsi" msgstr "" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" msgstr "" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "" @@ -1214,7 +1319,7 @@ msgstr "" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "" @@ -1261,33 +1366,37 @@ msgstr "" msgid "To:" msgstr "" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "" + +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this sprint to this issue." msgstr "" -#: taiga/projects/issues/api.py:143 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this status to this issue." msgstr "" -#: taiga/projects/issues/api.py:147 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this severity to this issue." msgstr "" -#: taiga/projects/issues/api.py:151 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this priority to this issue." msgstr "" -#: taiga/projects/issues/api.py:155 +#: taiga/projects/issues/api.py:176 msgid "You don't have permissions to set this type to this issue." msgstr "" @@ -1309,10 +1418,6 @@ msgstr "" msgid "priority" msgstr "" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1337,10 +1442,23 @@ msgstr "" msgid "external reference" msgstr "" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "" @@ -1352,8 +1470,8 @@ msgstr "" msgid "estimated finish date" msgstr "" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "" @@ -1382,215 +1500,220 @@ msgstr "" msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" +msgid "Involved" msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" +msgid "All" msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "" - -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "" - -#: taiga/projects/notifications/models.py:59 -msgid "created date time" +msgid "None" msgstr "" #: taiga/projects/notifications/models.py:61 -msgid "updated date time" +msgid "created date time" msgstr "" #: taiga/projects/notifications/models.py:63 +msgid "updated date time" +msgstr "" + +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2062,7 +2185,7 @@ msgid "" "[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" msgstr "" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "" @@ -2086,65 +2209,68 @@ msgstr "" msgid "You can't leave the project if there are no more owners" msgstr "" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "" - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." msgstr "" #: taiga/projects/tasks/models.py:56 @@ -2503,11 +2629,17 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:174 +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:254 #, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +msgid "Generating the user story #{ref} - {subject}" msgstr "" #: taiga/projects/userstories/models.py:37 @@ -2556,34 +2688,34 @@ msgid "There's no task status with that id" msgstr "" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "" @@ -2596,141 +2728,141 @@ msgstr "" msgid "Important dates" msgstr "" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "" - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "" - -#: taiga/users/api.py:184 -msgid "Invalid password length at least 6 charaters needed" -msgstr "" - -#: taiga/users/api.py:187 -msgid "Invalid current password" -msgstr "" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "" + +#: taiga/users/api.py:256 taiga/users/api.py:262 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" -#: taiga/users/api.py:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" -#: taiga/users/models.py:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "" -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -#: taiga/users/models.py:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "" -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "" -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "" diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index f8c322b1..25b57f95 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: @@ -12,10 +12,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-17 07:24+0000\n" -"Last-Translator: David Barragán \n" -"Language-Team: Spanish (http://www.transifex.com/projects/p/taiga-back/" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,40 +35,41 @@ msgstr "Tipo de registro inválido" msgid "invalid login type" msgstr "Tipo de login inválido" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "nombre de usuario no válido" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Son necesarios. 255 caracteres o menos (letras, números y /./-/_)" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "Nombre de usuario no disponible" -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "Email no disponible" -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "El token no pertenece a ninguna invitación válida." -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "Este usuario ya está registrado." -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "Ya existe una membresía asociada a este usuario." -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "Error al crear un nuevo usuario " #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "Token inválido" @@ -345,12 +346,12 @@ msgstr "Error de integridad por argumentos incorrectos o inválidos" msgid "Precondition error" msgstr "Error por incumplimiento de precondición" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "Error en los típos de parámetros de filtrado" -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "'project' debe ser un valor entero." @@ -559,32 +560,32 @@ msgstr "error importando las etiquetas" msgid "error importing timelines" msgstr "error importando los timelines" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" no se ha encontrado en este proyecto" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "Contiene attributos personalizados inválidos." -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "Nombre duplicado para el proyecto" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "Erro generando el volcado de datos del proyecto" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "Error cargando el volcado de datos del proyecto" @@ -823,11 +824,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Tu proyecto ha sido importado" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "Se requiere autenticación" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "nombre" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "URL del icono" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "web" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "descripción" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "Siguiente URL" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "clave secreta para cifrar los tokens de aplicación" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "usuario" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "aplicación" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "nombre completo" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "dirección de email" @@ -835,12 +886,14 @@ msgstr "dirección de email" msgid "comment" msgstr "comentario" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "fecha de creación" @@ -908,7 +961,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "El payload no es un json válido" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "El proyecto no existe" @@ -916,29 +970,81 @@ msgstr "El proyecto no existe" msgid "Bad signature" msgstr "Firma errónea" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "El payload no es una application/x-www-form-urlencoded válida" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "El payload no es válido" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "El elemento referenciado no existe" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "El estado no existe" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "Estado cambiado desde un commit de BitBucket" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Información inválida de Issue" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Petición creada por [@{bitbucket_user_name}]({bitbucket_user_url} \"Ver el " +"perfil de @{bitbucket_user_name} en BitBucket\") desde BitBucket.\n" +"Petición de origen en BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Ir a 'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "Petición creada desde BitBucket." + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Información de comentario de Issue inválida" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Comentario de [@{bitbucket_user_name}]({bitbucket_user_url} \"\"Ver el " +"perfil de @{bitbucket_user_name} en BitBucket\") desde BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"Comentario desde BitBucket:\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -950,15 +1056,11 @@ msgstr "" "de GitHub [{commit_id}]({commit_url} \"Ver commit '{commit_id} - " "{commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "Estado cambiado a través de un commit en GitHub." -#: 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/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -974,15 +1076,11 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "Petición creada a través de GitHub." -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "Información de comentario de Issue inválida" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -998,7 +1096,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -1009,21 +1107,49 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "Estado cambiado desde un commit de GitLab" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "Creado desde Gitlab" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Comentario de [@{gitlab_user_name}]({gitlab_user_url} \"Ver el perfil de " +"@{gitlab_user_name}'s en GitLab\") desde GitLab.\n" +"Petición de origen de GitLab: [gl#{number} - {subject}]({gitlab_url} \"Ir a " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"Comentario desde GitLab:\n" +"\n" +"{message}" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "Ver proyecto" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "Ver sprints" @@ -1031,240 +1157,232 @@ msgstr "Ver sprints" msgid "View user stories" msgstr "Ver historias de usuarios" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "Ver tareas" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "Ver peticiones" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "Ver páginas del wiki" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "Ver enlaces del wiki" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "Votar peticiones" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "Solicitar afiliación" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "Añadir historias de usuario al proyecto" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "Agregar comentarios a historia de usuario" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "Agregar comentarios a tareas" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "Añadir peticiones" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "Añadir comentarios a peticiones" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "Agregar pagina wiki" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "Modificar pagina wiki" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "Agregar enlace wiki" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "Modificar enlace wiki" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "Añadir sprint" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "Modificar sprint" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "Borrar sprint" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "Ver historia de usuario" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "Agregar historia de usuario" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "Modificar historia de usuario" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "Borrar historia de usuario" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "Agregar tarea" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "Modificar tarea" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "Borrar tarea" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "Añadir petición" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "Modificar petición" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "Borrar petición" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "Borrar pagina wiki" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "Borrar enlace wiki" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "Modificar proyecto" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "Agregar miembro" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "Eliminar miembro" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "Eliminar proyecto" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "Administrar valores de proyecto" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "Administrar roles" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "Nombre de plantilla invalido" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "Descripción de plantilla invalida" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 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:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "No tienes suficientes permisos para ver esto." #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" +msgid "Partial updates are not supported" msgstr "La actualización parcial no está soportada." #: taiga/projects/attachments/api.py:62 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:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "Dueño" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "Proyecto" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "típo de contenido" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "id de objeto" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "fecha modificada" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "archivo adjunto" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "sha1" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "está desactualizado" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "descripción" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "orden" @@ -1277,33 +1395,44 @@ msgid "Jitsi" msgstr "Jitsi" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "Personalizado" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "nombre" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Texto" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Texto multilínea" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "Fecha" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "tipo" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "valores" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "historia de usuario" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "tarea" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "petición" @@ -1383,7 +1512,7 @@ msgstr "borrado" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "No asignado" @@ -1430,33 +1559,37 @@ msgstr "De:" msgid "To:" msgstr "A:" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "contenido" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "nota de bloqueo" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "sprint" + +#: taiga/projects/issues/api.py:160 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:143 +#: taiga/projects/issues/api.py:164 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:147 +#: taiga/projects/issues/api.py:168 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:151 +#: taiga/projects/issues/api.py:172 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:155 +#: taiga/projects/issues/api.py:176 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." @@ -1478,10 +1611,6 @@ msgstr "gravedad" msgid "priority" msgstr "prioridad" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "tipo" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1506,10 +1635,23 @@ msgstr "asignado a" msgid "external reference" msgstr "referencia externa" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "recuento" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "Likes" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "Like" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "slug" @@ -1521,8 +1663,8 @@ msgstr "fecha estimada de comienzo" msgid "estimated finish date" msgstr "fecha estimada de finalización" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "está cerrada" @@ -1553,216 +1695,221 @@ 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:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "email" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "creado el" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "token" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "texto extra de la invitación" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "orden del usuario" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "El usuario ya es miembro del proyecto" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "puntos por defecto" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "estado de historia por defecto" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "estado de tarea por defecto" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "prioridad por defecto" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "gravedad por defecto" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "estado de petición por defecto" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "tipo de petición por defecto" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "miembros" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "total de sprints" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "puntos de historia totales" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "panel de backlog activado" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "panel de kanban activado" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "panel de wiki activo" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "panel de peticiones activo" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "sistema de videoconferencia" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" -msgstr "salt de la sala de videoconferencia" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "datos extra de videoconferencia" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "creación de plantilla" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "permisos de anónimo" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "permisos de usuario" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "privado" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "colores de etiquetas" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "configuración de modulos" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "archivado" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "color" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "limite del trabajo en progreso" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "valor" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "rol por defecto para el propietario" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "opciones por defecto" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "estatuas de historias" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "puntos" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "estatus de tareas" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "estados de petición" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "tipos de petición" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "gravedades" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "roles" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "No observado" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "Observado" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "Ignorado" +msgid "None" +msgstr "" -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "observadores" - -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "created date time" msgstr "fecha y hora de creación" -#: taiga/projects/notifications/models.py:61 +#: taiga/projects/notifications/models.py:63 msgid "updated date time" msgstr "fecha y hora de actualización" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "entradas del histórico" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "usuarios notificados" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "Observado" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" "Ya existe una política de notificación para este usuario en el proyecto." +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "Valor inválido para el nivel de notificación" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2470,7 +2617,7 @@ msgstr "" "\n" "[%(project)s] Borrada la página del wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "Los observadores tienen usuarios invalidos" @@ -2495,66 +2642,69 @@ msgid "You can't leave the project if there are no more owners" msgstr "" "No puedes abandonar este proyecto si no existen mas propietarios del mismo" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "La dirección de email ya está en uso." -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "Rol inválido para el proyecto" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "El número total de sprints debe ser mayor o igual a cero" - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "Opciones por defecto" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "Estados de historia de usuario" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "Puntos" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "Estado de tareas" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "Estados de peticion" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "Tipos de petición" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "Prioridades" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "Gravedades" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "Roles" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "Sprint futuro" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "Final de proyecto" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." -msgstr "No tienes permisos para añadir/modificar esta tarea." +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +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:107 +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:110 +msgid "You don't have permissions to set this status to this task." +msgstr "No tienes permisos para asignar este estado a esta tarea." #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -2964,14 +3114,20 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:174 -#, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." msgstr "" -"Generada la historia de usuario [US #{ref} - {subject}](:us:{ref} \"US " -"#{ref} - {subject}\")" +"No tienes permisos para asignar este sprint a esta historia de usuario." + +#: taiga/projects/userstories/api.py:160 +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:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" +msgstr "Generada la historia de usuario #{ref} - {subject}" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -3019,34 +3175,34 @@ msgid "There's no task status with that id" msgstr "No existe ningún estado de tarea con este id" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "Votos" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "votos" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "Voto" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "el parámetro 'content' es obligatório" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "el parámetro 'project_id' es obligatório" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "última modificación por" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "Comprueba la API de histórico para obtener el diff exacto" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "Información personal" @@ -3059,65 +3215,65 @@ msgstr "Permisos" msgid "Important dates" msgstr "datos importántes" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "Nombre de usuario o email no válidos" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "¡Correo enviado con éxito!" - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "token inválido" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "La contraseña actual es obligatoria." - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "La nueva contraseña es obligatoria" - -#: taiga/users/api.py:184 -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:187 -msgid "Invalid current password" -msgstr "Contraseña actual inválida" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "Argumentos incompletos" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "Formato de imagen no válido" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "Email duplicado" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "Email no válido" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Nombre de usuario o email no válidos" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "¡Correo enviado con éxito!" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "token inválido" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "La contraseña actual es obligatoria." + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "La nueva contraseña es obligatoria" + +#: taiga/users/api.py:205 +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:208 +msgid "Invalid current password" +msgstr "Contraseña actual inválida" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Argumentos incompletos" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Formato de imagen no válido" + +#: taiga/users/api.py:256 taiga/users/api.py:262 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:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 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:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "es superusuario" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3125,24 +3281,24 @@ msgstr "" "Otorga todos los permisos a este usuario sin necesidad de hacerlo " "explicitamente." -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "nombre de usuario" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Obligatorio. 30 caracteres o menos. Letras, números y /./-/_" -#: taiga/users/models.py:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "Introduce un nombre de usuario válido" -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "activo" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3150,55 +3306,55 @@ msgstr "" "Denota a los usuarios activos. Desmárcalo para dar de baja/borrar a un " "usuario." -#: taiga/users/models.py:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "biografía" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "foto" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "fecha de registro" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "idioma por defecto" -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "tema por defecto" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "zona horaria por defecto" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "añade color a las etiquetas" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "token de email" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "nueva dirección de email" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "permisos" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "no válido" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "Nombre de usuario inválido. Prueba con otro." -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "Nombre de usuario o contraseña inválidos." diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po index d38113fe..139549cb 100644 --- a/taiga/locale/fi/LC_MESSAGES/django.po +++ b/taiga/locale/fi/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: @@ -9,10 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-09 07:47+0000\n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" "Last-Translator: Taiga Dev Team \n" -"Language-Team: Finnish (http://www.transifex.com/projects/p/taiga-back/" +"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -32,41 +32,42 @@ msgstr "väärä rekisterin tyyppi" msgid "invalid login type" msgstr "väärä kirjautumistyyppi" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "tuntematon käyttäjänimi" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "Vaaditaan. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "Käyttäjänimi on varattu." -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "Sähköposti on jo varattu." -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "Tunniste ei vastaa mihinkään avoimeen kutsuun." -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "Käyttäjä on jo rekisteröitynyt." -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "Jäsenyys on jo olemassa." -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "Virhe käyttäjän luonnissa." #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "Väärä tunniste" @@ -331,12 +332,12 @@ msgstr "Integrity Error for wrong or invalid arguments" msgid "Precondition error" msgstr "Precondition error" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "Error in filter params types." -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "'project' must be an integer value." @@ -546,32 +547,32 @@ msgstr "virhe avainsanojen sisäänlukemisessa" msgid "error importing timelines" msgstr "virhe aikajanojen tuonnissa" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" ei löytynyt tästä projektista" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "Virheellinen sisältä, pitää olla muodossa {\"avain\": \"arvo\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "Sisältää vieheellisiä omia kenttiä." -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "Nimi on tuplana projektille" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "Virhe tiedoston luonnissa" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "Virhe tiedoston latauksessa" @@ -807,11 +808,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Projetkisi tiedosto on luettu sisään" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "nimi" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "kuvaus" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "koko nimi" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "sähköpostiosoite" @@ -819,12 +870,14 @@ msgstr "sähköpostiosoite" msgid "comment" msgstr "kommentti" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "luontipvm" @@ -894,7 +947,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "The payload is not a valid json" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "Projektia ei löydy" @@ -902,29 +956,66 @@ msgstr "Projektia ei löydy" msgid "Bad signature" msgstr "Virheellinen allekirjoitus" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "The payload is not a valid application/x-www-form-urlencoded" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "The payload is not valid" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "Viitattu elementtiä ei löydy" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "Tilaa ei löydy" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "Tila muutettu BitBucket kommitilla" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Virheellinen pyynnön tieto" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Virheellinen pyynnön kommentin tieto" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -935,15 +1026,11 @@ msgstr "" "@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "Tila muutettu GitHub commitin toimesta." -#: 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/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -960,15 +1047,11 @@ msgstr "" "\n" "{description}" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "Pyyntö luotu GitHubista" -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "Virheellinen pyynnön kommentin tieto" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -985,7 +1068,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -996,21 +1079,40 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "Tila muutettu GitLab kommitilla" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "Luotu GitLabissa" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "Katso projektia" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "Katso virstapylvästä" @@ -1018,240 +1120,232 @@ msgstr "Katso virstapylvästä" msgid "View user stories" msgstr "Katso käyttäjätarinoita" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "Katso tehtäviä" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "Katso pyyntöjä" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "Katso wiki-sivuja" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "Katso wiki-linkkejä" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "Äänestä pyyntöjä" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "Pyydä jäsenyyttä" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "Lisää käyttäjätarina projektiin" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "Lisää kommentteja käyttäjätarinoihin" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "Lisää kommentteja tehtäviin" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "Lisää pyyntöjä" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "Lisää kommentteja pyyntöihin" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "Lisää wiki-sivu" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "Muokkaa wiki-sivua" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "Lisää wiki-linkki" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "Muokkaa wiki-linkkiä" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "Lisää virstapylväs" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "Muokkaa virstapyvästä" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "Poista virstapylväs" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "Katso käyttäjätarinaa" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "Lisää käyttäjätarina" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "Muokkaa käyttäjätarinaa" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "Poista käyttäjätarina" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "Lisää tehtävä" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "Muokkaa tehtävää" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "Poista tehtävä" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "Lisää pyyntö" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "Muokkaa pyyntöä" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "Poista pyyntö" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "Poista wiki-sivu" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "Poista wiki-linkki" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "Muokkaa projekti" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "Lisää jäsen" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "Poista jäsen" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "Poista projekti" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "Hallinnoi projektin arvoja" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "Hallinnoi rooleja" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "Virheellinen mallipohjan nimi" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "Virheellinen mallipohjan kuvaus" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 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:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "Sinulla ei ole oikeuksia nähdä tätä." #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" -msgstr "Osittaiset päivitykset eivät ole tuettuna." +msgid "Partial updates are not supported" +msgstr "" #: taiga/projects/attachments/api.py:62 msgid "Project ID not matches between object and project" msgstr "Projekti ID ei vastaa kohdetta ja projektia" -#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "omistaja" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "projekti" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "sisältötyyppi" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "objekti ID" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "muokkauspvm" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "liite" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "on poistettu" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "kuvaus" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "order" @@ -1264,33 +1358,44 @@ msgid "Jitsi" msgstr "Jitsi" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "nimi" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "tyyppi" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "arvot" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "käyttäjätarina" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "tehtävä" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "pyyntö" @@ -1370,7 +1475,7 @@ msgstr "poistettu" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "Tekijä puuttuu" @@ -1417,33 +1522,37 @@ msgstr "Keneltä:" msgid "To:" msgstr "Kenelle:" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "sisältö" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "suljettu muistiinpano" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "" + +#: taiga/projects/issues/api.py:160 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:143 +#: taiga/projects/issues/api.py:164 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:147 +#: taiga/projects/issues/api.py:168 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:151 +#: taiga/projects/issues/api.py:172 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:155 +#: taiga/projects/issues/api.py:176 msgid "You don't have permissions to set this type to this issue." msgstr "Sinulla ei ole oikeutta asettaa tyyppiä tälle pyyntö." @@ -1465,10 +1574,6 @@ msgstr "vakavuus" msgid "priority" msgstr "kiireellisyys" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "tyyppi" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1493,10 +1598,23 @@ msgstr "tekijä" msgid "external reference" msgstr "ulkoinen viittaus" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "hukka-aika" @@ -1508,8 +1626,8 @@ msgstr "arvioitu alkupvm" msgid "estimated finish date" msgstr "arvioitu loppupvm" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "on suljettu" @@ -1538,215 +1656,220 @@ msgstr "'{param}' parametri on pakollinen" msgid "'project' parameter is mandatory" msgstr "'project' parametri on pakollinen" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "sähköposti" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "luo täällä" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "tunniste" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "kutsun lisäteksti" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "käyttäjäjärjestys" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "Käyttäjä on jo projektin jäsen" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "oletuspisteet" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "oletus Kt tila" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "oletus tehtävän tila" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "oletus kiireellisyys" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "oletus vakavuus" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "oletus pyynnön tila" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "oletus pyyntö tyyppi" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "jäsenet" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "virstapyväitä yhteensä" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "käyttäjätarinan yhteispisteet" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "aktiivinen odottavien paneeli" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "aktiivinen kanban-paneeli" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "aktiivinen wiki-paneeli" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "aktiivinen pyyntöpaneeli" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "videokokous järjestelmä" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" -msgstr "videokokousjärjestelmän suola" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "luo mallipohja" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "vieraan oikeudet" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "käyttäjän oikeudet" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "on yksityinen" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "avainsanojen värit" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "moduulien asetukset" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "on arkistoitu" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "väri" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "työn alla olevien max" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "arvo" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "oletus omistajan rooli" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "oletus optiot" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "kt tilat" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "pisteet" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "tehtävän tilat" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "pyyntöjen tilat" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "pyyntötyypit" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "kiireellisyydet" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "vakavuudet" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "roolit" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "Ei seuraa" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "Seuraa" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "Ohittaa" +msgid "None" +msgstr "" -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "vahdit" - -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "created date time" msgstr "luontipvm" -#: taiga/projects/notifications/models.py:61 +#: taiga/projects/notifications/models.py:63 msgid "updated date time" msgstr "päivityspvm" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "historian kohteet" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "ilmoita käyttäjille" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Ilmoita olemassaolosta määritellyille käyttäjille ja projektille" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2464,7 +2587,7 @@ msgstr "" "\n" "[%(project)s] Poistettiin wiki-sivu \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "Vahdit sisältävät virheellisiä käyttäjiä" @@ -2488,66 +2611,69 @@ msgstr "versio" msgid "You can't leave the project if there are no more owners" msgstr "Et voi jättää projektia, jos olet ainoa omistaja" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "Sähköpostiosoite on jo käytössä" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "Virheellinen rooli projektille" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "Virstapylväitä yhteensä pitää olla vähintään 0." - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "Oletusoptiot" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "Käyttäjätarinatilat" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "Pisteet" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "Tehtävien tilat" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "Pyyntöjen tilat" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "pyyntötyypit" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "Kiireellisyydet" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "Vakavuudet" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "Roolit" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "Tuleva kierros" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "Projektin loppu" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." -msgstr "Sinulla ei ole oikeuksia lisätä tai muokata tätä tehtävää." +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." +msgstr "" #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -2954,14 +3080,18 @@ msgstr "Tuoteomistaja" msgid "Stakeholder" msgstr "Sidosryhmä" -#: taiga/projects/userstories/api.py:174 -#, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" msgstr "" -"Luodaan käyttäjätarinaa [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -3009,34 +3139,34 @@ msgid "There's no task status with that id" msgstr "En löydä tehtävän tilaa tällä id:llä" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "Ääniä" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "ääniä" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "Äänestä" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "'content' parametri on pakollinen" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametri on pakollinen" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "viimeksi muokannut" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "Henkilökohtaiset tiedot" @@ -3049,147 +3179,147 @@ msgstr "Oikeudet" msgid "Important dates" msgstr "Tärkeät päivämäärät" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "Tuntematon käyttäjänimi tai sähköposti" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "Sähköposti lähetetty." - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "Tunniste on virheellinen" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "Nykyinen salasanaparametri tarvitaan" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "Uusi salasanaparametri tarvitaan" - -#: taiga/users/api.py:184 -msgid "Invalid password length at least 6 charaters needed" -msgstr "Salasanan pitää olla vähintään 6 merkkiä pitkä" - -#: taiga/users/api.py:187 -msgid "Invalid current password" -msgstr "Virheellinen nykyinen salasana" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "Puutteelliset argumentit" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "Väärä kuvaformaatti" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "Sähköposti on jo olemassa" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "Virheellinen sähköposti" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Tuntematon käyttäjänimi tai sähköposti" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "Sähköposti lähetetty." + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Tunniste on virheellinen" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Nykyinen salasanaparametri tarvitaan" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Uusi salasanaparametri tarvitaan" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Salasanan pitää olla vähintään 6 merkkiä pitkä" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "Virheellinen nykyinen salasana" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Puutteelliset argumentit" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Väärä kuvaformaatti" + +#: taiga/users/api.py:256 taiga/users/api.py:262 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:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "Virheellinen, oletko varma että tunniste on oikea?" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "pääkäyttäjän status" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 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:100 +#: taiga/users/models.py:102 msgid "username" msgstr "käyttäjänimi" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Vaaditaan. Korkeintaan 30merkkiä. Kirjaimet, numerot ja merkit /./-/_ " "sallittuja" -#: taiga/users/models.py:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "Anna olemassa oleva käyttäjänimi." -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "aktiivinen" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 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:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "kuva" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "liittymispvm" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "oletuskieli" -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "oletus aikavyöhyke" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "väritä avainsanat" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "sähköpostitunniste" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "uusi sähköpostiosoite" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "oikeudet" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "virheellinen" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "Tuntematon käyttäjänimi, yritä uudelleen." -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "Käyttäjätunnus tai salasana eivät ole oikein." diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po index a5de978e..b4601396 100644 --- a/taiga/locale/fr/LC_MESSAGES/django.po +++ b/taiga/locale/fr/LC_MESSAGES/django.po @@ -1,13 +1,15 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: # Alain Poirier , 2015 # David Barragán , 2015 +# Djyp Forest Fortin , 2015 # Florent B. , 2015 # Louis-Michel Couture , 2015 # Matthieu Durocher , 2015 +# naekos , 2015 # Nlko , 2015 # Stéphane Mor , 2015 # William Godin , 2015 @@ -15,10 +17,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-12 21:30+0000\n" -"Last-Translator: Nlko \n" -"Language-Team: French (http://www.transifex.com/projects/p/taiga-back/" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -38,41 +40,42 @@ msgstr "type d'inscription invalide" msgid "invalid login type" msgstr "type d'identifiant invalide" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "nom d'utilisateur invalide" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "Requis. 255 caractères ou moins. Lettres, chiffres et caractères /./-/_'" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "Ce nom d'utilisateur est déjà utilisé." -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "Cette adresse email est déjà utilisée." -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "Le jeton ne correspond à aucune invitation." -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "Cet utilisateur est déjà inscrit." -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "Cet utilisateur est déjà membre." -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "Erreur à la création du nouvel utilisateur." #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "Jeton invalide" @@ -352,12 +355,12 @@ msgstr "Erreur d'intégrité ou arguments invalides" msgid "Precondition error" msgstr "Erreur de précondition" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "Erreur dans les types de paramètres de filtres" -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "'project' doit être une valeur entière." @@ -431,6 +434,26 @@ msgid "" " \n" " " msgstr "" +"\n" +" Support de " +"Taiga :\n" +" %(support_url)s\n" +"
\n" +" Nous contacter :" +"\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Groupe de " +"discussion :\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -554,32 +577,32 @@ msgstr "erreur lors de l'importation des mots-clés" msgid "error importing timelines" msgstr "erreur lors de l'import des timelines" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" non trouvé dans the projet" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "Format non valide. Il doit être de la forme {\"cle\": \"valeur\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "Contient des champs personnalisés non valides." -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "Nom dupliqué pour ce projet" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "Error dans la génération du dump du projet" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "Erreur au chargement du dump du projet" @@ -598,6 +621,16 @@ msgid "" "

The Taiga Team

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

Project dump generated

\n" +"

Bonjour %(user)s,

\n" +"

Votre dump du projet %(project)s a bie nété généré.

\n" +"

Vous pouvez le télécharger ici :

\n" +" Télécharger le dump\n" +"

Ce fichier sera supprimé le %(deletion_date)s.

\n" +"

L'équipe Taiga

\n" +" " #: taiga/export_import/templates/emails/dump_project-body-text.jinja:1 #, python-format @@ -615,6 +648,18 @@ msgid "" "---\n" "The Taiga Team\n" msgstr "" +"\n" +"Bonjour %(user)s,\n" +"\n" +"Votre dump du projet %(project)s a bien été créé. Vous pouvez le télécharger " +"ici :\n" +"\n" +"%(url)s\n" +"\n" +"Ce fichier sera supprimé le %(deletion_date)s.\n" +"\n" +"---\n" +"L'équipe Taiga\n" #: taiga/export_import/templates/emails/dump_project-subject.jinja:1 #, python-format @@ -635,6 +680,16 @@ msgid "" "

The Taiga Team

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

%(error_message)s

\n" +"

Bonjour %(user)s,

\n" +"

Votre projet %(project)s n'a pas été exporté correctement.

\n" +"

L'administrateur système de Taiga en a été informé.
Merci " +"d'essayer à nouveau ou de contacter le support à\n" +" %(support_email)s

\n" +"

L'équipe Taiga

\n" +" " #: taiga/export_import/templates/emails/export_error-body-text.jinja:1 #, python-format @@ -652,6 +707,18 @@ msgid "" "---\n" "The Taiga Team\n" msgstr "" +"\n" +"Bonjour %(user)s,\n" +"\n" +"%(error_message)s\n" +"Votre projet %(project)s n'a pas été exporté correctement.\n" +"\n" +"L'administrateur système de Taiga en a été informé.\n" +"\n" +"Merci d'essayer à nouveau ou de contacter le support à %(support_email)s\n" +"\n" +"---\n" +"L'équipe Taiga\n" #: taiga/export_import/templates/emails/export_error-subject.jinja:1 #, python-format @@ -730,11 +797,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Votre projet à été importé" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "nom" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "description" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "utilisateur" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "Nom complet" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "Adresse email" @@ -742,12 +859,14 @@ msgstr "Adresse email" msgid "comment" msgstr "Commentaire" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "Date de création" @@ -815,7 +934,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "Le payload n'est pas un json valide" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "Le projet n'existe pas" @@ -823,29 +943,66 @@ msgstr "Le projet n'existe pas" msgid "Bad signature" msgstr "Signature non valide" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "Le payload n'est pas au format application/x-www-form-urlencoded" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "Le payload n'est pas valide" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "L'élément référencé n'existe pas" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "L'état n'existe pas" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "Statut changé depuis un commit BitBucket" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Information incorrecte sur le problème" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Ignoré" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -853,15 +1010,11 @@ msgid "" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." msgstr "" -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "Statut changé depuis un commit GitHub." -#: 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/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -872,15 +1025,11 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "Suivi de problème créé à partir de GitHub." -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "Ignoré" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -891,7 +1040,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -902,21 +1051,40 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "Statut changé depuis un commit GitLab" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "Créé à partir de GitLab" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "Consulter le projet" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "Voir les jalons" @@ -924,240 +1092,232 @@ msgstr "Voir les jalons" msgid "View user stories" msgstr "Voir les histoires utilisateur" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "Consulter les tâches" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "Voir les problèmes" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "Consulter les pages Wiki" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "Consulter les liens Wiki" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "Voter pour les problèmes" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "Demander à devenir membre" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "Ajouter l'histoire utilisateur au projet" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "Ajouter des commentaires aux histoires utilisateur" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "Ajouter des commentaires à une tâche" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "Ajouter des problèmes" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "Ajouter des commentaires aux problèmes" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "Ajouter une page Wiki" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "Modifier une page Wiki" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "Ajouter un lien Wiki" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "Modifier un lien Wiki" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "Ajouter un jalon" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "Modifier le jalon" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "Supprimer le jalon" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "Voir l'histoire utilisateur" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "Ajouter une histoire utilisateur" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "Modifier l'histoire utilisateur" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "Supprimer l'histoire utilisateur" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "Ajouter une tâche" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "Modifier une tâche" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "Supprimer une tâche" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "Ajouter un problème" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "Modifier le problème" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "Supprimer le problème" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "Supprimer une page Wiki" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "Supprimer un lien Wiki" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "Modifier le projet" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "Ajouter un membre" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "Supprimer un membre" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "Supprimer le projet" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "Administrer les paramètres du projet" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "Administrer les rôles" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "Nom de modèle non valide" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "Description du modèle non valide" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 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:499 +#: taiga/projects/api.py:511 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:47 -msgid "Non partial updates not supported" -msgstr "Les mises à jour non partielles ne sont pas supportées" +msgid "Partial updates are not supported" +msgstr "" #: taiga/projects/attachments/api.py:62 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:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "propriétaire" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "projet" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "type du contenu" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "identifiant de l'objet" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "état modifié" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "pièces jointes" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "est obsolète" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "description" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "ordre" @@ -1170,33 +1330,44 @@ msgid "Jitsi" msgstr "Jitsi" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "nom" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Texte" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Texte multi-ligne" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "type" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "valeurs" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "histoire utilisateur" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "tâche" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "problème" @@ -1276,7 +1447,7 @@ msgstr "supprimé" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "Non assigné" @@ -1323,33 +1494,37 @@ msgstr "De :" msgid "To:" msgstr "A :" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "contenu" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "note bloquée" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "sprint" + +#: taiga/projects/issues/api.py:160 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:143 +#: taiga/projects/issues/api.py:164 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:147 +#: taiga/projects/issues/api.py:168 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:151 +#: taiga/projects/issues/api.py:172 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:155 +#: taiga/projects/issues/api.py:176 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." @@ -1371,10 +1546,6 @@ msgstr "sévérité" msgid "priority" msgstr "priorité" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "type" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1399,10 +1570,23 @@ msgstr "assigné à" msgid "external reference" msgstr "référence externe" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "slug" @@ -1414,8 +1598,8 @@ msgstr "date de démarrage estimée" msgid "estimated finish date" msgstr "date de fin estimée" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "est fermé" @@ -1444,215 +1628,220 @@ msgstr "'{param}' paramètre obligatoire" msgid "'project' parameter is mandatory" msgstr "'project' paramètre obligatoire" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "email" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "Créé le" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "jeton" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "Text supplémentaire de l'invitation" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" -msgstr "" +msgstr "classement utilisateur" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "L'utilisateur est déjà un membre du projet" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "Points par défaut" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "statut de l'HU par défaut" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "Etat par défaut des tâches" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "Priorité par défaut" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "Sévérité par défaut" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "statut du problème par défaut" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "type de problème par défaut" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "membres" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "total des jalons" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "total des points d'histoire" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "panneau backlog actif" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "panneau kanban actif" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "panneau wiki actif" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "panneau problèmes actif" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "plateforme de vidéoconférence" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" -msgstr "salt pour la salle de vidéoconférence" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "Modèle de création" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "Permissions anonymes" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "Permission de l'utilisateur" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "est privé" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "couleurs des tags" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "Configurations des modules" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "est archivé" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "couleur" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "limite de travail en cours" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "valeur" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "rôle par défaut du propriétaire" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "options par défaut" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "statuts des us" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "points" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "états des tâches" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "statuts des problèmes" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "types de problèmes" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "priorités" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "sévérités" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "rôles" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "Non surveillé" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "Sous surveillance" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "Ignoré" +msgid "None" +msgstr "" -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "observateurs" - -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "created date time" msgstr "date de création" -#: taiga/projects/notifications/models.py:61 +#: taiga/projects/notifications/models.py:63 msgid "updated date time" msgstr "date de mise à jour" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "entrées dans l'historique" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "notifier les utilisateurs" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "La notification existe pour l'utilisateur et le projet spécifiés" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2142,7 +2331,7 @@ msgstr "" "\n" "[%(project)s] Page Wiki \"%(page)s\" supprimée\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "La liste des observateurs contient des utilisateurs invalides" @@ -2167,66 +2356,69 @@ msgid "You can't leave the project if there are no more owners" msgstr "" "Vous ne pouvez pas quitter le projet si il n'y a plus d'autres propriétaires" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "Adresse email déjà existante" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "Rôle non valide pour le projet" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "Le nombre de jalons doit être supérieur ou égal à zéro" - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "Options par défaut" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "Etats de la User Story" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "Points" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "Etats des tâches" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "Statuts des problèmes" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "Types de problèmes" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "Priorités" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "Sévérités" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "Rôles" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "Sprint futurs" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "Fin du projet" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." -msgstr "Vous n'avez pas les permissions pour ajouter / modifier cette tâche" +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." +msgstr "" #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -2349,6 +2541,14 @@ msgid "" "

The Taiga Team

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

Vous avez été ajouté-e à un projet

\n" +"

Bonjour %(full_name)s,
vous avez été ajouté-e au projet " +"%(project)s

\n" +" Aller " +"au projet\n" +"

L'équipe de Taiga

\n" +" " #: taiga/projects/templates/emails/membership_notification-body-text.jinja:1 #, python-format @@ -2613,14 +2813,18 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Participant" -#: taiga/projects/userstories/api.py:174 -#, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" msgstr "" -"Génération de l'histoire utilisateur [HU #{ref} - {subject}](:us:{ref} \"HU " -"#{ref} - {subject}\")" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -2668,34 +2872,34 @@ msgid "There's no task status with that id" msgstr "Il n'y a pas de statut de tâche avec cet id" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "Votes" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "votes" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "vote" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "'content' paramètre obligatoire" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "'project_id' paramètre obligatoire" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "dernier modificateur" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "Informations personnelles" @@ -2708,66 +2912,66 @@ msgstr "Permissions" msgid "Important dates" msgstr "Dates importantes" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "Nom d'utilisateur ou email non valide" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "Mail envoyé avec succès!" - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "Jeton invalide" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "Paramètre 'mot de passe actuel' requis" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "Paramètre 'nouveau mot de passe' requis" - -#: taiga/users/api.py:184 -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:187 -msgid "Invalid current password" -msgstr "Mot de passe actuel incorrect" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "arguments manquants" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "format de l'image non valide" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "Email dupliquée" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "Email non valide" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Nom d'utilisateur ou email non valide" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "Mail envoyé avec succès!" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Jeton invalide" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Paramètre 'mot de passe actuel' requis" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Paramètre 'nouveau mot de passe' requis" + +#: taiga/users/api.py:205 +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:208 +msgid "Invalid current password" +msgstr "Mot de passe actuel incorrect" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "arguments manquants" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "format de l'image non valide" + +#: taiga/users/api.py:256 taiga/users/api.py:262 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:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "Invalide, êtes-vous sûre que le jeton est correct ?" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "statut superutilisateur" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -2775,25 +2979,25 @@ msgstr "" "Indique que l'utilisateur a toutes les permissions sans avoir à lui les " "donner explicitement" -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "nom d'utilisateur" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 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:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "Entrez un nom d'utilisateur valide" -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "actif" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2801,55 +3005,55 @@ 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:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "biographie" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "photo" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "date d'inscription" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "langage par défaut" -#: taiga/users/models.py:122 -msgid "default theme" -msgstr "" - #: taiga/users/models.py:124 +msgid "default theme" +msgstr "thème par défaut" + +#: taiga/users/models.py:126 msgid "default timezone" msgstr "Fuseau horaire par défaut" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "changer la couleur des tags" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "jeton email" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "nouvelle adresse email" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "permissions" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "invalide" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom." -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "Aucun utilisateur avec ce nom ou ce mot de passe." diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..80f6b8ed --- /dev/null +++ b/taiga/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,3894 @@ +# taiga-back.taiga. +# Copyright (C) 2014-2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +# Andrea Raimondi , 2015 +# David Barragán , 2015 +# luca corsato , 2015 +# Marco Somma , 2015 +# Marco Vito Moscaritolo , 2015 +# Vittorio Della Rossa , 2015 +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/" +"language/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: taiga/auth/api.py:99 +msgid "Public register is disabled." +msgstr "Registro pubblico disabilitato" + +#: taiga/auth/api.py:132 +msgid "invalid register type" +msgstr "Tipo di registro invalido" + +#: taiga/auth/api.py:145 +msgid "invalid login type" +msgstr "Tipo di login invalido" + +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 +msgid "invalid username" +msgstr "Username non valido" + +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "" +"Sono richiesti 255 caratteri, o meno, contenenti: lettere, numeri e " +"caratteri /./-/_ " + +#: taiga/auth/services.py:73 +msgid "Username is already in use." +msgstr "Il nome utente appena scelto è già utilizzato." + +#: taiga/auth/services.py:76 +msgid "Email is already in use." +msgstr "L'email inserita è già utilizzata." + +#: taiga/auth/services.py:92 +msgid "Token not matches any valid invitation." +msgstr "Il token non corrisponde a nessun invito valido" + +#: taiga/auth/services.py:120 +msgid "User is already registered." +msgstr "L'Utente è già registrato." + +#: taiga/auth/services.py:144 +msgid "Membership with user is already exists." +msgstr "L'utente risulta già associato." + +#: taiga/auth/services.py:170 +msgid "Error on creating new user." +msgstr "Errore nella creazione dell'utente." + +#: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 +msgid "Invalid token" +msgstr "Token non valido" + +#: taiga/base/api/fields.py:268 +msgid "This field is required." +msgstr "Questo campo è obbligatorio." + +#: taiga/base/api/fields.py:269 taiga/base/api/relations.py:311 +msgid "Invalid value." +msgstr "Valore non valido." + +#: taiga/base/api/fields.py:453 +#, python-format +msgid "'%s' value must be either True or False." +msgstr "il valore di '%s' deve essere o vero o falso." + +#: taiga/base/api/fields.py:517 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "" +"Uno slug valido è composto da lettere, numeri, caratteri di sottolineatura o " +"trattini" + +#: taiga/base/api/fields.py:532 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "Seleziona un valore valido. %(value)s non è una scelta disponibile." + +#: taiga/base/api/fields.py:595 +msgid "Enter a valid email address." +msgstr "Inserisci un indirizzo e-mail valido." + +#: taiga/base/api/fields.py:637 +#, python-format +msgid "Date has wrong format. Use one of these formats instead: %s" +msgstr "La data non ha un formato valido. Usa uno dei formati disponibili: %s" + +#: taiga/base/api/fields.py:701 +#, python-format +msgid "Datetime has wrong format. Use one of these formats instead: %s" +msgstr "L'orario non ha un formato valido. Usa uno dei formati disponibili: %s" + +#: taiga/base/api/fields.py:771 +#, python-format +msgid "Time has wrong format. Use one of these formats instead: %s" +msgstr "Formato temporale errato. Usare uno dei seguenti formati: %s" + +#: taiga/base/api/fields.py:828 +msgid "Enter a whole number." +msgstr "Inserire il numero completo." + +#: taiga/base/api/fields.py:829 taiga/base/api/fields.py:882 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "Assicurati che il valore sia minore o uguale a %(limit_value)s." + +#: taiga/base/api/fields.py:830 taiga/base/api/fields.py:883 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "Assicurati che il valore sia maggiore o uguale a %(limit_value)s." + +#: taiga/base/api/fields.py:860 +#, python-format +msgid "\"%s\" value must be a float." +msgstr "il valore \"%s\" deve essere un valore \"float\"." + +#: taiga/base/api/fields.py:881 +msgid "Enter a number." +msgstr "Inserisci un numero" + +#: taiga/base/api/fields.py:884 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Assicurati che non ci siano più di %s cifre in totale." + +#: taiga/base/api/fields.py:885 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Assicurati che non ci siano più di %s decimali." + +#: taiga/base/api/fields.py:886 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Assicurati che non ci siano più di %s cifre prima del punto decimale." + +#: taiga/base/api/fields.py:953 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" +"Non è stato caricato nessun file. Controlla il tipo di codifica nella scheda." + +#: taiga/base/api/fields.py:954 +msgid "No file was submitted." +msgstr "Nessun file caricato." + +#: taiga/base/api/fields.py:955 +msgid "The submitted file is empty." +msgstr "Il file caricato è vuoto." + +#: taiga/base/api/fields.py:956 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "" +"Assicurati che il nome del file abbiamo al massimo %(max)d caratteri (ne ha " +"%(length)d)." + +#: taiga/base/api/fields.py:957 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" +"Carica il file oppure controlla la casella deselezionata. Non entrambi. " + +#: taiga/base/api/fields.py:997 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Carica un'immagina valida. Il file caricato potrebbe non essere un'immagine " +"o l'immagine potrebbe essere corrotta. " + +#: taiga/base/api/pagination.py:115 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "La pagina non è 'last', né può essere convertita come int." + +#: taiga/base/api/pagination.py:119 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Pagina (%(page_number)s) invalida: %(message)s" + +#: taiga/base/api/permissions.py:61 +msgid "Invalid permission definition." +msgstr "Definizione di permesso non valida." + +#: taiga/base/api/relations.py:221 +#, python-format +msgid "Invalid pk '%s' - object does not exist." +msgstr "pk '%s' invalido - l'oggetto non esiste" + +#: taiga/base/api/relations.py:222 +#, python-format +msgid "Incorrect type. Expected pk value, received %s." +msgstr "Inserimento scorretto. Atteso un valore pk, ricevuto %s." + +#: taiga/base/api/relations.py:310 +#, python-format +msgid "Object with %s=%s does not exist." +msgstr "L'oggetto con %s=%s non esiste." + +#: taiga/base/api/relations.py:346 +msgid "Invalid hyperlink - No URL match" +msgstr "Hyperlink invalido - nessun URL abbinato" + +#: taiga/base/api/relations.py:347 +msgid "Invalid hyperlink - Incorrect URL match" +msgstr "Hyperlink invalido - l'URL abbinato non è corretto" + +#: taiga/base/api/relations.py:348 +msgid "Invalid hyperlink due to configuration error" +msgstr "URL invalido a causa di un errore di configurazione" + +#: taiga/base/api/relations.py:349 +msgid "Invalid hyperlink - object does not exist." +msgstr "Hyperlink invalido - l'oggetto non esiste" + +#: taiga/base/api/relations.py:350 +#, python-format +msgid "Incorrect type. Expected url string, received %s." +msgstr "Inserimento scorretto. Attesa una stringa con URL, ricevuto %s." + +#: taiga/base/api/serializers.py:296 +msgid "Invalid data" +msgstr "Dati non validi" + +#: taiga/base/api/serializers.py:388 +msgid "No input provided" +msgstr "Non è stato fornito nessun input" + +#: taiga/base/api/serializers.py:548 +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:559 +msgid "Expected a list of items." +msgstr "Ci si aspetta una lista di oggetti." + +#: taiga/base/api/views.py:100 +msgid "Not found" +msgstr "Non trovato" + +#: taiga/base/api/views.py:103 +msgid "Permission denied" +msgstr "Permesso negato" + +#: taiga/base/api/views.py:451 +msgid "Server application error" +msgstr "Errore sul server" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "Errore di connessione" + +#: taiga/base/exceptions.py:53 +msgid "Malformed request." +msgstr "Richiesta composta erroneamente." + +#: taiga/base/exceptions.py:58 +msgid "Incorrect authentication credentials." +msgstr "Le credenziali non sono corrette." + +#: taiga/base/exceptions.py:63 +msgid "Authentication credentials were not provided." +msgstr "Le credenziali per l'autenticazione non sono state fornite." + +#: taiga/base/exceptions.py:68 +msgid "You do not have permission to perform this action." +msgstr "Non hai il permesso per eseguire l'azione. " + +#: taiga/base/exceptions.py:73 +#, python-format +msgid "Method '%s' not allowed." +msgstr "Metodo '%s' non permesso." + +#: taiga/base/exceptions.py:81 +msgid "Could not satisfy the request's Accept header" +msgstr "" +"Non è possibile soddisfare la richiesta di accettazione dell'intestazione." + +#: taiga/base/exceptions.py:90 +#, python-format +msgid "Unsupported media type '%s' in request." +msgstr "Nella richiesta è presente un contenuto media '%s' non supportato." + +#: taiga/base/exceptions.py:98 +msgid "Request was throttled." +msgstr "La richiesta è stata soppressa" + +#: taiga/base/exceptions.py:99 +#, python-format +msgid "Expected available in %d second%s." +msgstr "Disponibile in %d secondi%s." + +#: taiga/base/exceptions.py:113 +msgid "Unexpected error" +msgstr "Errore inaspettato" + +#: taiga/base/exceptions.py:125 +msgid "Not found." +msgstr "Non trovato." + +#: taiga/base/exceptions.py:130 +msgid "Method not supported for this endpoint." +msgstr "Metodo non supportato dall'endpoint." + +#: taiga/base/exceptions.py:138 taiga/base/exceptions.py:146 +msgid "Wrong arguments." +msgstr "Argomento errato." + +#: taiga/base/exceptions.py:150 +msgid "Data validation error" +msgstr "Errore di validazione dei dati" + +#: taiga/base/exceptions.py:162 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "Errore di integrità causato da un argomento invalido o sbagliato" + +#: taiga/base/exceptions.py:169 +msgid "Precondition error" +msgstr "Errore di precondizione" + +#: taiga/base/filters.py:80 +msgid "Error in filter params types." +msgstr "Errore nel filtro del tipo di parametri." + +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 +msgid "'project' must be an integer value." +msgstr "'Progetto' deve essere un valore intero." + +#: taiga/base/tags.py:25 +msgid "tags" +msgstr "tags" + +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "Taiga" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "Seguici su Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "Prendi il codice su GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "Visita il nostro sito" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "Taiga.io" + +#: taiga/base/templates/emails/base-body-html.jinja:423 +#: taiga/base/templates/emails/hero-body-html.jinja:397 +#: taiga/base/templates/emails/updates-body-html.jinja:459 +#, python-format +msgid "" +"\n" +" Taiga Support:\n" +" %(support_url)s\n" +"
\n" +" Contact us:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Mailing list:\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " +msgstr "" +"\n" +"Supporto Taiga:\n" +"\n" +"" +"%(support_url)s\n" +"\n" +"
\n" +"\n" +"Contact us:\n" +"\n" +"\n" +"\n" +"%(support_email)s\n" +"\n" +"\n" +"\n" +"
\n" +"\n" +"Mailing list:\n" +"\n" +"\n" +"\n" +"%(mailing_list_url)s\n" +"\n" +"" + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Sei stato Taigatizzato" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

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

Sei stato Taigato!

\n" +"\n" +"

Benvenuto in Taiga, uno strumento open source per la gestione agile dei " +"progetti

" + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "[Taiga] Aggiornamenti" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "Aggiornamenti" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

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

commento:

\n" +"\n" +"

%(comment)s

" + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" +"\n" +"Commento: %(comment)s" + +#: taiga/export_import/api.py:103 +msgid "We needed at least one role" +msgstr "C'è bisogno di almeno un ruolo" + +#: taiga/export_import/api.py:197 +msgid "Needed dump file" +msgstr "E' richiesto un file di dump" + +#: taiga/export_import/api.py:204 +msgid "Invalid dump format" +msgstr "Formato di dump invalido" + +#: taiga/export_import/dump_service.py:96 +msgid "error importing project data" +msgstr "Errore nell'importazione del progetto dati" + +#: taiga/export_import/dump_service.py:109 +msgid "error importing lists of project attributes" +msgstr "Errore nell'importazione della lista degli attributi di progetto" + +#: taiga/export_import/dump_service.py:114 +msgid "error importing default project attributes values" +msgstr "" +"Errore nell'importazione dei valori predefiniti degli attributi del progetto." + +#: taiga/export_import/dump_service.py:124 +msgid "error importing custom attributes" +msgstr "Errore nell'importazione degli attributi personalizzati" + +#: taiga/export_import/dump_service.py:129 +msgid "error importing roles" +msgstr "Errore nell'importazione i ruoli" + +#: taiga/export_import/dump_service.py:144 +msgid "error importing memberships" +msgstr "Errore nell'importazione delle iscrizioni" + +#: taiga/export_import/dump_service.py:149 +msgid "error importing sprints" +msgstr "errore nell'importazione degli sprints" + +#: taiga/export_import/dump_service.py:154 +msgid "error importing wiki pages" +msgstr "Errore nell'importazione delle pagine wiki" + +#: taiga/export_import/dump_service.py:159 +msgid "error importing wiki links" +msgstr "Errore nell'importazione dei link di wiki" + +#: taiga/export_import/dump_service.py:164 +msgid "error importing issues" +msgstr "errore nell'importazione dei problemi" + +#: taiga/export_import/dump_service.py:169 +msgid "error importing user stories" +msgstr "Errore nell'importazione delle user story" + +#: taiga/export_import/dump_service.py:174 +msgid "error importing tasks" +msgstr "Errore nell'importazione dei compiti " + +#: taiga/export_import/dump_service.py:179 +msgid "error importing tags" +msgstr "Errore nell'importazione dei tags" + +#: taiga/export_import/dump_service.py:183 +msgid "error importing timelines" +msgstr "Errore nell'importazione delle timelines" + +#: taiga/export_import/serializers.py:163 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" non è stato trovato in questo progetto" + +#: taiga/export_import/serializers.py:428 +#: taiga/projects/custom_attributes/serializers.py:103 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Contenuto errato. Deve essere {\"key\": \"value\",...}" + +#: taiga/export_import/serializers.py:443 +#: taiga/projects/custom_attributes/serializers.py:118 +msgid "It contain invalid custom fields." +msgstr "Contiene campi personalizzati invalidi." + +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 +msgid "Name duplicated for the project" +msgstr "Il nome del progetto è duplicato" + +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 +msgid "Error generating project dump" +msgstr "Errore nella creazione del dump di progetto" + +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 +msgid "Error loading project dump" +msgstr "Errore nel caricamento del dump di progetto" + +#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump generated

\n" +"

Hello %(user)s,

\n" +"

Your dump from project %(project)s has been correctly generated.\n" +"

You can download it here:

\n" +" Download the dump file\n" +"

This file will be deleted on %(deletion_date)s.

\n" +"

The Taiga Team

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

E' stato generato il dump di progetto

\n" +"\n" +"

Salve %(user)s,

\n" +"\n" +"

Il dump del tuo progetto %(project)s è stato creato correttamente

\n" +"\n" +"

Puoi scaricarlo qui:

\n" +"\n" +"Scarica l file\n" +"\n" +"

Questa file sarà cancellato il %(deletion_date)s.

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/export_import/templates/emails/dump_project-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your dump from project %(project)s has been correctly generated. You can " +"download it here:\n" +"\n" +"%(url)s\n" +"\n" +"This file will be deleted on %(deletion_date)s.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Salve %(user)s,\n" +"\n" +"\n" +"\n" +"Il dump del tuo progetto %(project)s e' stato creato correttamente. Puoi " +"scaricarlo qui:\n" +"\n" +"\n" +"\n" +"%(url)s\n" +"\n" +"\n" +"\n" +"Questo file verrà cancellato il %(deletion_date)s.\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "[%(project)s] Il dump del tuo progetto è stato creato correttamente" + +#: taiga/export_import/templates/emails/export_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project %(project)s has not been exported correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"\n" +"

Salve %(user)s,

\n" +"\n" +"

Il tuo progetto %(project)s non è stato esportato correttamente.

\n" +"\n" +"

Gli amministratori di sistema di Taiga sono stati informati.
Per " +"favore, fai di nuovo un tentativo o contatta il team di supporto a:\n" +"\n" +"%(support_email)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/export_import/templates/emails/export_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"Your project %(project)s has not been exported correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Salve %(user)s,\n" +"\n" +"\n" +"\n" +"%(error_message)s\n" +"\n" +"Il tuo progetto %(project)s non è stato esportato correttamente.\n" +"\n" +"\n" +"\n" +"Gli amministratori di sistema di Taiga sono stati informati.\n" +"\n" +"\n" +"\n" +"Per favore, fai di nuovo un tentativo o contatta il team di supporto a: " +"%(support_email)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "[%(project)s] %(error_subject)s" + +#: taiga/export_import/templates/emails/import_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project has not been importer correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"\n" +"

Salve %(user)s,

\n" +"\n" +"

Il tuo progetto non è stato importato correttamente

\n" +"\n" +"

Gli amministratori di sistema di Taiga sono stati informati.
Per " +"favore, prova di nuovo o contatta il team di supporto a:\n" +"\n" +"%(support_email)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/export_import/templates/emails/import_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Your project has not been importer correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Salve %(user)s,\n" +"\n" +"\n" +"\n" +"%(error_message)s\n" +"\n" +"\n" +"\n" +"\n" +"\n" +"\n" +"\n" +"Gli amministratori di sistema di Taiga sono stati informati\n" +"\n" +"\n" +"\n" +"Per favore riprova di nuovo o contatta il nostro team di supporto a " +"%(support_email)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "[Taiga] %(error_subject)s" + +#: taiga/export_import/templates/emails/load_dump-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump imported

\n" +"

Hello %(user)s,

\n" +"

Your project dump has been correctly imported.

\n" +" Go to %(project)s\n" +"

The Taiga Team

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

Il dump di progetto è stato importato

\n" +"\n" +"

Salve%(user)s,

\n" +"\n" +"

Il dump del tuo progetto è stato importato correttamente.

\n" +"\n" +"Vai a %(project)s\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your project dump has been correctly imported.\n" +"\n" +"You can see the project %(project)s here:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Salve %(user)s,\n" +"\n" +"\n" +"\n" +"Il dump del tuo progetto è stato importato correttamente\n" +"\n" +"\n" +"\n" +"Puoi vedere il progetto %(project)s qui:\n" +"\n" +"\n" +"\n" +"%(url)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "[%(project)s] Il dump del tuo progetto è stato importato" + +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "E' richiesta l'autenticazione" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "nome" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "Url dell'icona" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "web" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "descrizione" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "Url successivo" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "chiave segreta per cifrare i token dell'applicazione" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "utente" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "applicazione" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 +msgid "full name" +msgstr "Nome completo" + +#: taiga/feedback/models.py:25 taiga/users/models.py:108 +msgid "email address" +msgstr "Inserisci un indirizzo e-mail valido." + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "Commento" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 +msgid "created date" +msgstr "data creata" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

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

Feedback

\n" +"

Taiga ha ricevuto un feedback da %(full_name)s <%(email)s>

" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

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

Commento

\n" +"\n" +"

%(comment)s

" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "Informazioni aggiuntive" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" +"---------\n" +"\n" +"- Da: %(full_name)s <%(email)s>\n" +"\n" +"---------\n" +"\n" +"- Commento:\n" +"\n" +"%(comment)s\n" +"\n" +"---------" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "- Maggiori informazioni:" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" +"\n" +"\n" +"[Taiga] Hai un feedback da %(full_name)s <%(email)s>\n" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "Il carico non è un json valido" + +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 +msgid "The project doesn't exist" +msgstr "Il progetto non esiste" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "Firma non valida" + +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 +msgid "The referenced element doesn't exist" +msgstr "L'elemento di riferimento non esiste" + +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 +msgid "The status doesn't exist" +msgstr "Lo stato non esiste" + +#: taiga/hooks/bitbucket/event_hooks.py:97 +msgid "Status changed from BitBucket commit" +msgstr "Lo stato è stato modificato a seguito di un commit di BitBucket" + +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Informazione sul problema non valida" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Problema creato da [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") da BitBucket.\n" +"\n" +"Origine del problema su BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Go to 'bb#{number} - {subject}'\"):\n" +"\n" +"\n" +"\n" +"{description}" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "Problema creato da BItBucket" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Commento sul problema non valido" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Commento da [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") da BitBucket.\n" +"\n" +"Origine del problema da BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Go to 'bb#{number} - {subject}'\")\n" +"\n" +"\n" +"\n" +"{message}" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"Commento da BitBucket:\n" +"\n" +"\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 +#, python-brace-format +msgid "" +"Status changed by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +msgstr "" +"Stato cambiato da [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." + +#: taiga/hooks/github/event_hooks.py:107 +msgid "Status changed from GitHub commit." +msgstr "Lo stato è stato modificato da un commit su GitHub." + +#: taiga/hooks/github/event_hooks.py:157 +#, python-brace-format +msgid "" +"Issue created by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Problema creato da [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") su GitHub.\n" +"\n" +"Origine del problema su GitHub: [gh#{number} - {subject}]({github_url} \"Go " +"to 'gh#{number} - {subject}'\"):\n" +"\n" +"\n" +"\n" +"{description}" + +#: taiga/hooks/github/event_hooks.py:168 +msgid "Issue created from GitHub." +msgstr "Problema creato su GitHub." + +#: taiga/hooks/github/event_hooks.py:200 +#, python-brace-format +msgid "" +"Comment by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Commento da [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") su GitHub.\n" +"Origine del problema su GitHub: [gh#{number} - {subject}]({github_url} \"Go " +"to 'gh#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:211 +#, python-brace-format +msgid "" +"Comment From GitHub:\n" +"\n" +"{message}" +msgstr "" +"Commento su GitHub:\n" +"\n" +"\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:86 +msgid "Status changed from GitLab commit" +msgstr "Lo stato è stato modificato tramite commit su GitLab" + +#: taiga/hooks/gitlab/event_hooks.py:128 +msgid "Created from GitLab" +msgstr "Creato da GitLab" + +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Commento da [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") su GitLab.\n" +"\n" +"Origine del problema su GitLab: [gl#{number} - {subject}]({gitlab_url} \"Go " +"to 'gl#{number} - {subject}'\")\n" +"\n" +"\n" +"\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"Commento da GitLab:\n" +"\n" +"\n" +"\n" +"{message}" + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:51 +msgid "View project" +msgstr "Vedi progetto" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:53 +msgid "View milestones" +msgstr "Guarda le milestones" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "Guarda le storie utente" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 +msgid "View tasks" +msgstr "Guarda i compiti" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:68 +msgid "View issues" +msgstr "Guarda i problemi" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 +msgid "View wiki pages" +msgstr "Guarda le pagine wiki" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 +msgid "View wiki links" +msgstr "Guarda i lik di wiki" + +#: taiga/permissions/permissions.py:38 +msgid "Request membership" +msgstr "Richiedi l'iscrizione" + +#: taiga/permissions/permissions.py:39 +msgid "Add user story to project" +msgstr "Aggiungi una storia utente al progetto" + +#: taiga/permissions/permissions.py:40 +msgid "Add comments to user stories" +msgstr "Aggiungi dei commenti alle storia utente" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to tasks" +msgstr "Aggiungi dei commenti ai compiti" + +#: taiga/permissions/permissions.py:42 +msgid "Add issues" +msgstr "Aggiungi i problemi" + +#: taiga/permissions/permissions.py:43 +msgid "Add comments to issues" +msgstr "Aggiungi dei commenti ai problemi" + +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 +msgid "Add wiki page" +msgstr "Aggiungi una pagina wiki" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 +msgid "Modify wiki page" +msgstr "Modifica la pagina wiki" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 +msgid "Add wiki link" +msgstr "Aggiungi un link wiki" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 +msgid "Modify wiki link" +msgstr "Modifica il link di wiki" + +#: taiga/permissions/permissions.py:54 +msgid "Add milestone" +msgstr "Aggiungi una tappa" + +#: taiga/permissions/permissions.py:55 +msgid "Modify milestone" +msgstr "Modifica la tappa" + +#: taiga/permissions/permissions.py:56 +msgid "Delete milestone" +msgstr "Elimina la tappa" + +#: taiga/permissions/permissions.py:58 +msgid "View user story" +msgstr "Guarda la storia utente" + +#: taiga/permissions/permissions.py:59 +msgid "Add user story" +msgstr "Aggiungi una storia utente" + +#: taiga/permissions/permissions.py:60 +msgid "Modify user story" +msgstr "Modifica una storia utente" + +#: taiga/permissions/permissions.py:61 +msgid "Delete user story" +msgstr "Cancella una storia utente" + +#: taiga/permissions/permissions.py:64 +msgid "Add task" +msgstr "Aggiungi un compito" + +#: taiga/permissions/permissions.py:65 +msgid "Modify task" +msgstr "Modifica il compito" + +#: taiga/permissions/permissions.py:66 +msgid "Delete task" +msgstr "Elimina compito" + +#: taiga/permissions/permissions.py:69 +msgid "Add issue" +msgstr "Aggiungi un problema" + +#: taiga/permissions/permissions.py:70 +msgid "Modify issue" +msgstr "Modifica il problema" + +#: taiga/permissions/permissions.py:71 +msgid "Delete issue" +msgstr "Elimina il problema" + +#: taiga/permissions/permissions.py:76 +msgid "Delete wiki page" +msgstr "Elimina la pagina wiki" + +#: taiga/permissions/permissions.py:81 +msgid "Delete wiki link" +msgstr "Elimina la pagina wiki" + +#: taiga/permissions/permissions.py:85 +msgid "Modify project" +msgstr "Modifica il progetto" + +#: taiga/permissions/permissions.py:86 +msgid "Add member" +msgstr "Aggiungi un membro" + +#: taiga/permissions/permissions.py:87 +msgid "Remove member" +msgstr "Rimuovi il membro" + +#: taiga/permissions/permissions.py:88 +msgid "Delete project" +msgstr "Elimina il progetto" + +#: taiga/permissions/permissions.py:89 +msgid "Admin project values" +msgstr "Valori dell'amministratore del progetto" + +#: taiga/permissions/permissions.py:90 +msgid "Admin roles" +msgstr "Ruoli dell'amministratore" + +#: taiga/projects/api.py:202 +msgid "Not valid template name" +msgstr "Il nome del template non è valido" + +#: taiga/projects/api.py:205 +msgid "Not valid template description" +msgstr "La descrizione del template non è valida" + +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 +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:511 +msgid "You don't have permisions to see that." +msgstr "Non hai il permesso di vedere questo elemento." + +#: taiga/projects/attachments/api.py:47 +msgid "Partial updates are not supported" +msgstr "Aggiornamento non parziale non supportato" + +#: taiga/projects/attachments/api.py:62 +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:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 +#: taiga/userstorage/models.py:25 +msgid "owner" +msgstr "proprietario" + +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 +msgid "project" +msgstr "progetto" + +#: taiga/projects/attachments/models.py:56 +msgid "content type" +msgstr "tipo di contenuto" + +#: taiga/projects/attachments/models.py:58 +msgid "object id" +msgstr "ID dell'oggetto" + +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "data modificata" + +#: taiga/projects/attachments/models.py:69 +msgid "attached file" +msgstr "file allegato" + +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "sha1" + +#: taiga/projects/attachments/models.py:73 +msgid "is deprecated" +msgstr "non approvato" + +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 +msgid "order" +msgstr "ordine" + +#: taiga/projects/choices.py:21 +msgid "AppearIn" +msgstr "ApparIn" + +#: taiga/projects/choices.py:22 +msgid "Jitsi" +msgstr "Jitsi" + +#: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "Personalizzato" + +#: taiga/projects/choices.py:24 +msgid "Talky" +msgstr "Talky" + +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Testo" + +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Testo multi-linea" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "tipo" + +#: taiga/projects/custom_attributes/models.py:87 +msgid "values" +msgstr "valori" + +#: taiga/projects/custom_attributes/models.py:97 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 +msgid "user story" +msgstr "storia utente" + +#: taiga/projects/custom_attributes/models.py:112 +msgid "task" +msgstr "compito" + +#: taiga/projects/custom_attributes/models.py:127 +msgid "issue" +msgstr "problema" + +#: taiga/projects/custom_attributes/serializers.py:57 +msgid "Already exists one with the same name." +msgstr "Ne esiste già un altro con lo stesso nome" + +#: taiga/projects/history/api.py:70 +msgid "Comment already deleted" +msgstr "Il commento è già stato eliminato" + +#: taiga/projects/history/api.py:89 +msgid "Comment not deleted" +msgstr "Commento non eliminato" + +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "Cambiato" + +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "Creato" + +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "Eliminato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "%(role)s punti del ruolo" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:193 +msgid "from" +msgstr "da" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:199 +msgid "to" +msgstr "a" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "Aggiunto un nuovo allegato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "Allegato aggiornato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "non approvato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "accettato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "Allegato eliminato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "aggiunto" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "rimosso" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 +msgid "Unassigned" +msgstr "Non assegnato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:211 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "-eliminato-" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "a:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "da:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "Aggiunto" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "Modificato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "Eliminato" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "aggiunto:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "rimosso:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "Da:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "A:" + +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 +msgid "content" +msgstr "contenuto" + +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "nota bloccata" + +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "sprint" + +#: taiga/projects/issues/api.py:160 +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:164 +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:168 +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:172 +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:176 +msgid "You don't have permissions to set this type to this issue." +msgstr "Non hai i permessi per aggiungere questa tipologia a questo problema" + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "referenza" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "stato" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "criticità" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "priorità" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "tappa" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "data di conclusione" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "soggeto" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "assegnato a" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "referenza esterna" + +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "conta" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 +msgid "slug" +msgstr "lumaca" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "data stimata di inizio" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "data stimata di fine" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 +msgid "is closed" +msgstr "è concluso" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "disponibilità" + +#: taiga/projects/milestones/models.py:75 +msgid "The estimated start must be previous to the estimated finish." +msgstr "" +"La data stimata di inizio deve essere precedente alla data stimata di fine." + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "Non c'è nessuno sprint on questo ID" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "è bloccato" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "'{param}' parameter is mandatory" +msgstr "il parametro '{param}' è obbligatorio" + +#: taiga/projects/mixins/ordering.py:51 +msgid "'project' parameter is mandatory" +msgstr "il parametro 'project' è obbligatorio" + +#: taiga/projects/models.py:66 +msgid "email" +msgstr "email" + +#: taiga/projects/models.py:68 +msgid "create at" +msgstr "creato a " + +#: taiga/projects/models.py:70 taiga/users/models.py:130 +msgid "token" +msgstr "token" + +#: taiga/projects/models.py:76 +msgid "invitation extra text" +msgstr "testo ulteriore per l'invito" + +#: taiga/projects/models.py:79 +msgid "user order" +msgstr "ordine dell'utente" + +#: taiga/projects/models.py:89 +msgid "The user is already member of the project" +msgstr "L'utente è già membro del progetto" + +#: taiga/projects/models.py:104 +msgid "default points" +msgstr "punti predefiniti" + +#: taiga/projects/models.py:108 +msgid "default US status" +msgstr "stati predefiniti per le storie utente" + +#: taiga/projects/models.py:112 +msgid "default task status" +msgstr "stati predefiniti del compito" + +#: taiga/projects/models.py:115 +msgid "default priority" +msgstr "priorità predefinita" + +#: taiga/projects/models.py:118 +msgid "default severity" +msgstr "criticità predefinita" + +#: taiga/projects/models.py:122 +msgid "default issue status" +msgstr "stato predefinito del problema" + +#: taiga/projects/models.py:126 +msgid "default issue type" +msgstr "tipologia predefinita del problema" + +#: taiga/projects/models.py:147 +msgid "members" +msgstr "membri" + +#: taiga/projects/models.py:150 +msgid "total of milestones" +msgstr "tappe totali" + +#: taiga/projects/models.py:151 +msgid "total story points" +msgstr "punti totali della storia" + +#: taiga/projects/models.py:154 taiga/projects/models.py:614 +msgid "active backlog panel" +msgstr "pannello di backlog attivo" + +#: taiga/projects/models.py:156 taiga/projects/models.py:616 +msgid "active kanban panel" +msgstr "pannello kanban attivo" + +#: taiga/projects/models.py:158 taiga/projects/models.py:618 +msgid "active wiki panel" +msgstr "pannello wiki attivo" + +#: taiga/projects/models.py:160 taiga/projects/models.py:620 +msgid "active issues panel" +msgstr "pannello dei problemi attivo" + +#: taiga/projects/models.py:163 taiga/projects/models.py:623 +msgid "videoconference system" +msgstr "sistema di videoconferenza" + +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "ulteriori dati di videoconferenza " + +#: taiga/projects/models.py:170 +msgid "creation template" +msgstr "creazione del template" + +#: taiga/projects/models.py:173 +msgid "anonymous permissions" +msgstr "permessi anonimi" + +#: taiga/projects/models.py:177 +msgid "user permissions" +msgstr "permessi dell'utente" + +#: taiga/projects/models.py:180 +msgid "is private" +msgstr "è privato" + +#: taiga/projects/models.py:191 +msgid "tags colors" +msgstr "colori dei tag" + +#: taiga/projects/models.py:383 +msgid "modules config" +msgstr "configurazione dei moduli" + +#: taiga/projects/models.py:402 +msgid "is archived" +msgstr "è archivitato" + +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 +msgid "color" +msgstr "colore" + +#: taiga/projects/models.py:406 +msgid "work in progress limit" +msgstr "limite dei lavori in corso" + +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 +msgid "value" +msgstr "valore" + +#: taiga/projects/models.py:611 +msgid "default owner's role" +msgstr "ruolo proprietario predefinito" + +#: taiga/projects/models.py:627 +msgid "default options" +msgstr "opzioni predefinite " + +#: taiga/projects/models.py:628 +msgid "us statuses" +msgstr "stati della storia utente" + +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 +msgid "points" +msgstr "punti" + +#: taiga/projects/models.py:630 +msgid "task statuses" +msgstr "stati del compito" + +#: taiga/projects/models.py:631 +msgid "issue statuses" +msgstr "stati del probema" + +#: taiga/projects/models.py:632 +msgid "issue types" +msgstr "tipologie del problema" + +#: taiga/projects/models.py:633 +msgid "priorities" +msgstr "priorità" + +#: taiga/projects/models.py:634 +msgid "severities" +msgstr "criticità " + +#: taiga/projects/models.py:635 +msgid "roles" +msgstr "ruoli" + +#: taiga/projects/notifications/choices.py:28 +msgid "Involved" +msgstr "" + +#: taiga/projects/notifications/choices.py:29 +msgid "All" +msgstr "" + +#: taiga/projects/notifications/choices.py:30 +msgid "None" +msgstr "" + +#: taiga/projects/notifications/models.py:61 +msgid "created date time" +msgstr "tempo e data creati" + +#: taiga/projects/notifications/models.py:63 +msgid "updated date time" +msgstr "tempo e data aggiornati" + +#: taiga/projects/notifications/models.py:65 +msgid "history entries" +msgstr "inserimenti della storia" + +#: taiga/projects/notifications/models.py:68 +msgid "notify users" +msgstr "notifica utenti" + +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "Osservato" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 +msgid "Notify exists for specified user and project" +msgstr "La notifica esiste per l'utente e il progetto specificati" + +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "Valore non valido per il livello di notifica" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue updated

\n" +"

Hello %(user)s,
%(changer)s has updated an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +" " +msgstr "" +"\n" +"\n" +"

Problema aggiornato

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha aggiornato un problema su " +"%(project)s

\n" +"\n" +"

Problema #%(ref)s %(subject)s

\n" +"\n" +"Guarda il problema" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Issue updated\n" +"Hello %(user)s, %(changer)s has updated an issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"\n" +"Problema aggiornato\n" +"\n" +"Salve %(user)s, %(changer)s ha aggiornato un problema su %(project)s\n" +"\n" +"Vai al problema #%(ref)s %(subject)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha aggiornato il problema #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New issue created

\n" +"

Hello %(user)s,
%(changer)s has created a new issue on " +"%(project)s

\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +"

The Taiga Team

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

E' stato creato un nuovo problema

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha creato un nuovo problema su " +"%(project)s

\n" +"\n" +"

Problema #%(ref)s %(subject)s

\n" +"\n" +"Vai al problema\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New issue created\n" +"Hello %(user)s, %(changer)s has created a new issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"E' stato creato un nuovo problema\n" +"\n" +"Salve %(user)s, %(changer)s ha creato un nuovo problema %(project)s\n" +"\n" +"Vai al problema #%(ref)s %(subject)s su %(url)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha creato il problema #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Problema eliminato

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha eliminato un problema %(project)s\n" +"\n" +"

Issue #%(ref)s %(subject)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Issue deleted\n" +"Hello %(user)s, %(changer)s has deleted an issue on %(project)s\n" +"Issue #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Problema eliminato\n" +"\n" +"Salve %(user)s, %(changer)s ha eliminato un problema su %(project)s\n" +"\n" +"Problema #%(ref)s %(subject)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" +"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha eliminato il problema #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint updated

\n" +"

Hello %(user)s,
%(changer)s has updated an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See sprint\n" +" " +msgstr "" +"\n" +"\n" +"

Sprint aggiornato

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha aggiornato uno sprint su %(project)s\n" +"\n" +"

Sprint %(name)s

\n" +"\n" +"Vedi lo sprint" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Sprint updated\n" +"Hello %(user)s, %(changer)s has updated a sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +msgstr "" +"\n" +"\n" +"Sprint updated\n" +"\n" +"Salve %(user)s, %(changer)s ha aggiornato uno sprint su %(project)s\n" +"\n" +"Guarda lo sprint %(name)s a %(url)s\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha aggiornato lo sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New sprint created

\n" +"

Hello %(user)s,
%(changer)s has created a new sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See " +"sprint\n" +"

The Taiga Team

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

E' stato creato un nuovo sprint

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha creato un nuovo sprint su " +"%(project)s

\n" +"\n" +"

Sprint %(name)s

\n" +"\n" +"Vai allo " +"sprint\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New sprint created\n" +"Hello %(user)s, %(changer)s has created a new sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"E' stato creato un nuovo sprint\n" +"\n" +"Salve %(user)s, %(changer)s ha creato un nuovo sprint su %(project)s\n" +"\n" +"Guarda lo sprint %(name)s su %(url)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha creato lo sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +"

The Taiga Team

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

Sprint eliminato

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha eliminato uno sprint di %(project)s\n" +"\n" +"

Sprint %(name)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Sprint deleted\n" +"Hello %(user)s, %(changer)s has deleted an sprint on %(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Sprint eliminato\n" +"\n" +"Salve %(user)s, %(changer)s ha eliminato uno sprint di %(project)s\n" +"\n" +"Sprint %(name)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha eliminato uno sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task updated

\n" +"

Hello %(user)s,
%(changer)s has updated a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +" " +msgstr "" +"\n" +"\n" +"

Compito aggiornato

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha aggiornato un compito di %(project)s\n" +"\n" +"

Compito #%(ref)s %(subject)s

\n" +"\n" +"Guarda il compito" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Task updated\n" +"Hello %(user)s, %(changer)s has updated a task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"\n" +"Compito aggiornato\n" +"\n" +"Salve %(user)s, %(changer)s ha aggiornato un compito di %(project)s\n" +"\n" +"Guarda il compito #%(ref)s %(subject)s a %(url)s\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha aggiornato un compito #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New task created

\n" +"

Hello %(user)s,
%(changer)s has created a new task on " +"%(project)s

\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +"

The Taiga Team

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

E' stato creato un nuovo compito

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha creato un nuovo compito su " +"%(project)s

\n" +"\n" +"

Compito #%(ref)s %(subject)s

\n" +"\n" +"Vai al compito\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New task created\n" +"Hello %(user)s, %(changer)s has created a new task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"E' stato creato un nuovo compito\n" +"\n" +"Salve %(user)s, %(changer)s ha creato un nuovo compito su %(project)s\n" +"\n" +"Guarda il compito #%(ref)s %(subject)s a %(url)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha creato il compito #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Compito eliminato

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha eliminato un compito su %(project)s\n" +"\n" +"

Compito #%(ref)s %(subject)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Task deleted\n" +"Hello %(user)s, %(changer)s has deleted a task on %(project)s\n" +"Task #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Compito eliminato\n" +"\n" +"Hello %(user)s, %(changer)s ha eliminato un compito di %(project)s\n" +"\n" +"Compito #%(ref)s %(subject)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha eliminato il compito #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story updated

\n" +"

Hello %(user)s,
%(changer)s has updated a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +" " +msgstr "" +"\n" +"

User Story aggiornata

\n" +"

Ciao %(user)s,
%(changer)s ha aggiornato una storia utente in " +"%(project)s

\n" +"

Storia utente #%(ref)s %(subject)s

\n" +"Guarda la User Story" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"User story updated\n" +"Hello %(user)s, %(changer)s has updated a user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"User story aggiornata\n" +"Ciao %(user)s, %(changer)s ha aggiornato una storia utente in %(project)s\n" +"Guarda storia utente #%(ref)s %(subject)s in %(url)s\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha aggiornato la storia utente #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New user story created

\n" +"

Hello %(user)s,
%(changer)s has created a new user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +"

The Taiga Team

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

E' stata creata una nuovo storia utente

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha creato una nuovo storia utente su " +"%(project)s

\n" +"\n" +"

Storia utente #%(ref)s %(subject)s

\n" +"\n" +"Vai alla storia utente\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New user story created\n" +"Hello %(user)s, %(changer)s has created a new user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"E' stata creata una nuova storia utente\n" +"\n" +"Salve %(user)s, %(changer)s ha creato una nuova storia utente su " +"%(project)s\n" +"\n" +"Guarda la storia utente #%(ref)s %(subject)s su %(url)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha creato la storia utente #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Storia utente eliminata

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha eliminato una storia utente su " +"%(project)s

\n" +"\n" +"

Storia utente #%(ref)s %(subject)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"User Story deleted\n" +"Hello %(user)s, %(changer)s has deleted a user story on %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Storia utente eliminata\n" +"\n" +"Salve %(user)s, %(changer)s ha eliminato una storia utente di %(project)s\n" +"\n" +"Storia utente #%(ref)s %(subject)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha eliminato la storia utente #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki Page updated

\n" +"

Hello %(user)s,
%(changer)s has updated a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See Wiki Page\n" +" " +msgstr "" +"\n" +"\n" +"

Pagina wiki aggiornata

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha aggiornato una pagina wiki su " +"%(project)s

\n" +"\n" +"

Wiki page %(page)s

\n" +"\n" +"Guarda " +"la pagina wiki" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Wiki Page updated\n" +"\n" +"Hello %(user)s, %(changer)s has updated a wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +msgstr "" +"\n" +"\n" +"Pagina wiki aggiornata\n" +"\n" +"\n" +"\n" +"Salve %(user)s, %(changer)s ha aggiornato una pagina wiki su %(project)s\n" +"\n" +"\n" +"\n" +"Guarda la pagina wiki %(page)s a %(url)s\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha aggiornato la pagina wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New wiki page created

\n" +"

Hello %(user)s,
%(changer)s has created a new wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See " +"wiki page\n" +"

The Taiga Team

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

E' stata creata una nuova pagina wiki

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha creato una nuova pagina wiki su " +"%(project)s

\n" +"\n" +"

Wiki page %(page)s

\n" +"\n" +"Guarda la " +"pagina wiki\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New wiki page created\n" +"\n" +"Hello %(user)s, %(changer)s has created a new wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"E' stata creata una nuova pagina wiki\n" +"\n" +"\n" +"\n" +"Salve %(user)s, %(changer)s ha creato una nuova pagina wiki su %(project)s\n" +"\n" +"\n" +"\n" +"Guarda la pagina wiki %(page)s a %(url)s\n" +"\n" +"\n" +"\n" +"---\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha creato la pagina wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki page deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +"

The Taiga Team

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

Pagina wiki eliminata

\n" +"\n" +"

Salve %(user)s,
%(changer)s ha eliminato una pagina wiki su " +"%(project)s

\n" +"\n" +"

Pagina wiki %(page)s

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Wiki page deleted\n" +"\n" +"Hello %(user)s, %(changer)s has deleted a wiki page on %(project)s\n" +"\n" +"Wiki page %(page)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Pagina wiki eliminata\n" +"\n" +"\n" +"\n" +"Salve %(user)s, %(changer)s ha eliminato una pagina wiki su %(project)s\n" +"\n" +"\n" +"\n" +"Pagina wiki %(page)s\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"\n" +"[%(project)s] ha eliminato la pagina wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/validators.py:46 +msgid "Watchers contains invalid users" +msgstr "L'osservatore contiene un utente non valido" + +#: taiga/projects/occ/mixins.py:35 +msgid "The version must be an integer" +msgstr "La versione deve essere un intero" + +#: taiga/projects/occ/mixins.py:58 +msgid "The version parameter is not valid" +msgstr "Il parametro della versione non è valido" + +#: taiga/projects/occ/mixins.py:74 +msgid "The version doesn't match with the current one" +msgstr "La versione non corrisponde a quella corrente" + +#: taiga/projects/occ/mixins.py:93 +msgid "version" +msgstr "versione" + +#: taiga/projects/permissions.py:39 +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" + +#: taiga/projects/serializers.py:240 +msgid "Email address is already taken" +msgstr "L'indirizzo email è già usato" + +#: taiga/projects/serializers.py:252 +msgid "Invalid role for the project" +msgstr "Ruolo di progetto non valido" + +#: taiga/projects/serializers.py:397 +msgid "Default options" +msgstr "Opzioni predefinite" + +#: taiga/projects/serializers.py:398 +msgid "User story's statuses" +msgstr "Stati della storia utente" + +#: taiga/projects/serializers.py:399 +msgid "Points" +msgstr "Punti" + +#: taiga/projects/serializers.py:400 +msgid "Task's statuses" +msgstr "Stati del compito" + +#: taiga/projects/serializers.py:401 +msgid "Issue's statuses" +msgstr "Stati del problema" + +#: taiga/projects/serializers.py:402 +msgid "Issue's types" +msgstr "Tipologie del problema" + +#: taiga/projects/serializers.py:403 +msgid "Priorities" +msgstr "Priorità" + +#: taiga/projects/serializers.py:404 +msgid "Severities" +msgstr "Criticità" + +#: taiga/projects/serializers.py:405 +msgid "Roles" +msgstr "Ruoli" + +#: taiga/projects/services/stats.py:85 +msgid "Future sprint" +msgstr "Sprint futuri" + +#: taiga/projects/services/stats.py:102 +msgid "Project End" +msgstr "Termine di progetto" + +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +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:107 +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:110 +msgid "You don't have permissions to set this status to this task." +msgstr "Non hai i permessi per aggiungere questo stato a questo compito." + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "ordine della storia utente" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "ordine del pannello dei compiti" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "è sotto aspirina" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "Non c'è nessun compito con questo ID" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "qualcuno" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:11 +#, python-format +msgid "" +"\n" +"

You have been invited to Taiga!

\n" +"

Hi! %(full_name)s has sent you an invitation to join project " +"%(project)s in Taiga.
Taiga is a Free, open Source Agile Project " +"Management Tool.

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

Sei stato invitato in Taiga!

\n" +"\n" +"

Ciao! %(full_name)s ti ha mandato un invito per partecipare al progetto " +"%(project)s in Taiga.
Taiga è uno strumento per la gestione " +"agile e aperta di progetti.

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

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

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

E adesso qualche parola dal collega
che é stato così gentile " +"da invitarti

\n" +"\n" +"

%(extra)s

" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation to Taiga" +msgstr "Accetta l'invito in Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation" +msgstr "Accetta il tuo invito" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "The Taiga Team" +msgstr "Il Team di Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:6 +#, python-format +msgid "" +"\n" +"You, or someone you know, has invited you to Taiga\n" +"\n" +"Hi! %(full_name)s has sent you an invitation to join a project called " +"%(project)s which is being managed on Taiga, a Free, open Source Agile " +"Project Management Tool.\n" +msgstr "" +"\n" +"\n" +"Tu, o qualcuno che ti conosce, è stato invitato in Taiga\n" +"\n" +"\n" +"\n" +"Ciao! %(full_name)sti ha mandato un invito per partecipare al progetto " +"%(project)s gestito con Taiga, uno strumento per la gestione agile e aperta " +"di progetti.\n" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:12 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" +"\n" +"\n" +"E adesso qualche parola dal collega che é stato così gentile da invitarti\n" +"\n" +"\n" +"\n" +"%(extra)s" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:18 +msgid "Accept your invitation to Taiga following this link:" +msgstr "Accetta l'invito in Taiga seguendo il seguente link:" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:20 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" +"\n" +"\n" +"[Taiga] invito a partecipare al progetto '%(project)s'\n" + +#: taiga/projects/templates/emails/membership_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

You have been added to a project

\n" +"

Hello %(full_name)s,
you have been added to the project " +"%(project)s

\n" +" Go to " +"project\n" +"

The Taiga Team

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

Sei stato aggiunto ad un progetto

\n" +"\n" +"

Salve %(full_name)s,
sei stato aggiunto al progetto %(project)s

\n" +"\n" +"Vai al " +"progetto\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/projects/templates/emails/membership_notification-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"You have been added to a project\n" +"Hello %(full_name)s,you have been added to the project %(project)s\n" +"\n" +"See project at %(url)s\n" +msgstr "" +"\n" +"\n" +"Sei stato aggiunto ad un progetto\n" +"\n" +"Salve %(full_name)s, sei stato aggiunto al progetto %(project)s\n" +"\n" +"\n" +"\n" +"Guarda il progetto su %(url)s\n" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" +"\n" +"\n" +"[Taiga] aggiunto al progetto '%(project)s'\n" + +#. Translators: Name of scrum project template. +#: taiga/projects/translations.py:28 +msgid "Scrum" +msgstr "Scrum" + +#. Translators: Description of scrum project template. +#: taiga/projects/translations.py:30 +msgid "" +"The agile product backlog in Scrum is a prioritized features list, " +"containing short descriptions of all functionality desired in the product. " +"When applying Scrum, it's not necessary to start a project with a lengthy, " +"upfront effort to document all requirements. The Scrum product backlog is " +"then allowed to grow and change as more is learned about the product and its " +"customers" +msgstr "" +"Il prodotto agile \"backlog\" su Scrum è una lista di features che contiene " +"brevi descrizioni di tutte le funzionalità richieste nel prodotto. Quando " +"applichi Scrum non è necessario iniziare un progetto elencando " +"dettagliatamente tutta la documentazione necessaria. Il backlog Scrum, in " +"questo modo, può crescere e cambiare man mano che si apprendono le " +"caratteristiche del prodotto e dei suoi clienti." + +#. Translators: Name of kanban project template. +#: taiga/projects/translations.py:33 +msgid "Kanban" +msgstr "Kanban" + +#. Translators: Description of kanban project template. +#: taiga/projects/translations.py:35 +msgid "" +"Kanban is a method for managing knowledge work with an emphasis on just-in-" +"time delivery while not overloading the team members. In this approach, the " +"process, from definition of a task to its delivery to the customer, is " +"displayed for participants to see and team members pull work from a queue." +msgstr "" +"Kanban è un metodo per gestire il lavoro sulla conoscenza con un enfasi " +"sulle consegne da fare in tempo, mentre consente di non sovraccaricare i " +"membri del team. Con questo approccio il processo, dalla definizione di un " +"compito alla sua consegna ai clienti, viene mostrato ai partecipanti e ai " +"membri del team, in modo che possano organizzare il lavoro." + +#. Translators: User story point value (value = undefined) +#: taiga/projects/translations.py:43 +msgid "?" +msgstr "?" + +#. Translators: User story point value (value = 0) +#: taiga/projects/translations.py:45 +msgid "0" +msgstr "0" + +#. Translators: User story point value (value = 0.5) +#: taiga/projects/translations.py:47 +msgid "1/2" +msgstr "1/2" + +#. Translators: User story point value (value = 1) +#: taiga/projects/translations.py:49 +msgid "1" +msgstr "1" + +#. Translators: User story point value (value = 2) +#: taiga/projects/translations.py:51 +msgid "2" +msgstr "2" + +#. Translators: User story point value (value = 3) +#: taiga/projects/translations.py:53 +msgid "3" +msgstr "3" + +#. Translators: User story point value (value = 5) +#: taiga/projects/translations.py:55 +msgid "5" +msgstr "5" + +#. Translators: User story point value (value = 8) +#: taiga/projects/translations.py:57 +msgid "8" +msgstr "8" + +#. Translators: User story point value (value = 10) +#: taiga/projects/translations.py:59 +msgid "10" +msgstr "10" + +#. Translators: User story point value (value = 13) +#: taiga/projects/translations.py:61 +msgid "13" +msgstr "13" + +#. Translators: User story point value (value = 20) +#: taiga/projects/translations.py:63 +msgid "20" +msgstr "20" + +#. Translators: User story point value (value = 40) +#: taiga/projects/translations.py:65 +msgid "40" +msgstr "40" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:73 taiga/projects/translations.py:96 +#: taiga/projects/translations.py:112 +msgid "New" +msgstr "Nuovo" + +#. Translators: User story status +#: taiga/projects/translations.py:76 +msgid "Ready" +msgstr "Pronto" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:79 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 +msgid "In progress" +msgstr "In via di sviluppo" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:82 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 +msgid "Ready for test" +msgstr "Pronto per il test" + +#. Translators: User story status +#: taiga/projects/translations.py:85 +msgid "Done" +msgstr "Fatto" + +#. Translators: User story status +#: taiga/projects/translations.py:88 +msgid "Archived" +msgstr "Archiviato" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:102 taiga/projects/translations.py:118 +msgid "Closed" +msgstr "Concluso" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 +msgid "Needs Info" +msgstr "Necessita di informazioni" + +#. Translators: Issue status +#: taiga/projects/translations.py:122 +msgid "Postponed" +msgstr "Postposto " + +#. Translators: Issue status +#: taiga/projects/translations.py:124 +msgid "Rejected" +msgstr "Rifiutato" + +#. Translators: Issue type +#: taiga/projects/translations.py:132 +msgid "Bug" +msgstr "Bug" + +#. Translators: Issue type +#: taiga/projects/translations.py:134 +msgid "Question" +msgstr "Domanda" + +#. Translators: Issue type +#: taiga/projects/translations.py:136 +msgid "Enhancement" +msgstr "Miglioramento" + +#. Translators: Issue priority +#: taiga/projects/translations.py:144 +msgid "Low" +msgstr "Basso" + +#. Translators: Issue priority +#. Translators: Issue severity +#: taiga/projects/translations.py:146 taiga/projects/translations.py:159 +msgid "Normal" +msgstr "Normale" + +#. Translators: Issue priority +#: taiga/projects/translations.py:148 +msgid "High" +msgstr "Alto" + +#. Translators: Issue severity +#: taiga/projects/translations.py:155 +msgid "Wishlist" +msgstr "Lista dei desideri" + +#. Translators: Issue severity +#: taiga/projects/translations.py:157 +msgid "Minor" +msgstr "Minore" + +#. Translators: Issue severity +#: taiga/projects/translations.py:161 +msgid "Important" +msgstr "Importante" + +#. Translators: Issue severity +#: taiga/projects/translations.py:163 +msgid "Critical" +msgstr "Critico" + +#. Translators: User role +#: taiga/projects/translations.py:170 +msgid "UX" +msgstr "UX" + +#. Translators: User role +#: taiga/projects/translations.py:172 +msgid "Design" +msgstr "Design" + +#. Translators: User role +#: taiga/projects/translations.py:174 +msgid "Front" +msgstr "Front" + +#. Translators: User role +#: taiga/projects/translations.py:176 +msgid "Back" +msgstr "Back" + +#. Translators: User role +#: taiga/projects/translations.py:178 +msgid "Product Owner" +msgstr "Product Owner" + +#. Translators: User role +#: taiga/projects/translations.py:180 +msgid "Stakeholder" +msgstr "Stakeholder" + +#: taiga/projects/userstories/api.py:156 +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:160 +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:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" +msgstr "" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "ruolo" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "ordine del backlog" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "ordine dello sprint" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "data di termine" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "é un requisito del cliente " + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "é una richiesta del team" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "generato da un problema" + +#: taiga/projects/userstories/validators.py:28 +msgid "There's no user story with that id" +msgstr "Non c'è nessuna storia utente con questo ID" + +#: taiga/projects/validators.py:28 +msgid "There's no project with that id" +msgstr "Non c'è nessuno progetto con questo ID" + +#: taiga/projects/validators.py:37 +msgid "There's no user story status with that id" +msgstr "Non c'è nessuno stato della storia utente con questo ID" + +#: taiga/projects/validators.py:46 +msgid "There's no task status with that id" +msgstr "Non c'è nessuno stato del compito con questo ID" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:56 +msgid "Votes" +msgstr "Voti" + +#: taiga/projects/votes/models.py:55 +msgid "Vote" +msgstr "Voto" + +#: taiga/projects/wiki/api.py:66 +msgid "'content' parameter is mandatory" +msgstr "il parametro 'contenuto' è obbligatorio" + +#: taiga/projects/wiki/api.py:69 +msgid "'project_id' parameter is mandatory" +msgstr "Il parametro 'ID progetto' è obbligatorio" + +#: taiga/projects/wiki/models.py:37 +msgid "last modifier" +msgstr "ultima modificatore" + +#: taiga/projects/wiki/models.py:70 +msgid "href" +msgstr "href" + +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "Controlla le API della storie per la differenza esatta" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "Informazioni personali" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "Permessi" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "Date importanti" + +#: taiga/users/api.py:111 +msgid "Duplicated email" +msgstr "E-mail duplicata" + +#: taiga/users/api.py:113 +msgid "Not valid email" +msgstr "E-mail non valida" + +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Username o e-mail non validi" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "Mail inviata con successo!" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Token non valido" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "E' necessario il parametro della password corrente" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "E' necessario il parametro della nuovo password" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Lunghezza della password non valida, sono necessari almeno 6 caratteri" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "Password corrente non valida" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Argomento non valido" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Formato dell'immagine non valido" + +#: taiga/users/api.py:256 taiga/users/api.py:262 +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:289 taiga/users/api.py:297 taiga/users/api.py:300 +msgid "Invalid, are you sure the token is correct?" +msgstr "Non valido. Sicuro che il token sia corretto?" + +#: taiga/users/models.py:71 +msgid "superuser status" +msgstr "Stato del super-utente" + +#: taiga/users/models.py:72 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" +"Definisce che questo utente ha tutti i permessi senza assegnarglieli " +"esplicitamente." + +#: taiga/users/models.py:102 +msgid "username" +msgstr "nome utente" + +#: taiga/users/models.py:103 +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:106 +msgid "Enter a valid username." +msgstr "Inserisci un nome utente valido." + +#: taiga/users/models.py:109 +msgid "active" +msgstr "attivo" + +#: taiga/users/models.py:110 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" +"Definisce se questo utente debba essere trattato come attivo. Deseleziona " +"questo invece di eliminare gli account." + +#: taiga/users/models.py:116 +msgid "biography" +msgstr "biografia" + +#: taiga/users/models.py:119 +msgid "photo" +msgstr "fotografia" + +#: taiga/users/models.py:120 +msgid "date joined" +msgstr "data di inizio partecipazione" + +#: taiga/users/models.py:122 +msgid "default language" +msgstr "lingua predefinita" + +#: taiga/users/models.py:124 +msgid "default theme" +msgstr "tema predefinito" + +#: taiga/users/models.py:126 +msgid "default timezone" +msgstr "timezone predefinita" + +#: taiga/users/models.py:128 +msgid "colorize tags" +msgstr "colora i tag" + +#: taiga/users/models.py:133 +msgid "email token" +msgstr "token e-mail" + +#: taiga/users/models.py:135 +msgid "new email address" +msgstr "nuovo indirizzo e-mail" + +#: taiga/users/models.py:203 +msgid "permissions" +msgstr "permessi" + +#: taiga/users/serializers.py:62 +msgid "invalid" +msgstr "non valido" + +#: taiga/users/serializers.py:73 +msgid "Invalid username. Try with a different one." +msgstr "Nome utente non valido. Provane uno diverso." + +#: taiga/users/services.py:53 taiga/users/services.py:57 +msgid "Username or password does not matches user." +msgstr "Il nome utente o la password non corrispondono all'utente." + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Cambia la tua email

\n" +"\n" +"

Salve %(full_name)s,
per favore conferma la tua mail

\n" +"\n" +"conferma la " +"mail\n" +"\n" +"

Ignora questo messaggio se non lo hai richiesto

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Salve %(full_name)s, per favore conferma la tua mail\n" +"\n" +"\n" +"\n" +"%(url)s\n" +"\n" +"\n" +"\n" +"Ignora questo messaggio se non lo hai richiesto.\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "[Taiga] cambia la mail" + +#: taiga/users/templates/emails/password_recovery-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Recover your password

\n" +"

Hello %(full_name)s,
you asked to recover your password

\n" +" Recover your password\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Recupera la password

\n" +"\n" +"

Salve %(full_name)s,
hai richiesto di poter recuperare la " +"password

\n" +"\n" +"Recupera la password\n" +"\n" +"

Ignora questo messaggio se non lo hai richiesto.

\n" +"\n" +"

Il Team di Taiga

" + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"\n" +"Salve %(full_name)s, ha richiesto di poter recuperare la tua password \n" +"\n" +"\n" +"\n" +"%(url)s\n" +"\n" +"\n" +"\n" +"Ignora questo messaggio se non lo hai richiesto.\n" +"\n" +"\n" +"\n" +"---\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "[Taiga] recupero della password" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" \n" +" " +msgstr "" +"\n" +"\n" +"\n" +"

Grazie per esserti registrato in Taiga

\n" +"\n" +"

Speriamo ti possa divertire

\n" +"\n" +"

Abbiamo progettato Taiga perché volevamo uno strumento di gestione dei " +"progetti che rimanesse aperto sui nostri computer tutto il giorno, per " +"ricordarci perché amiamo collaborare programmare e progettare.

\n" +"\n" +"

Lo abbiamo costruito bello, elegante, semplice e divertente da usare - " +"senza tralasciare flessibilità e potenza.

\n" +"\n" +"Il Team di Taiga\n" +"\n" +"" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:23 +#, python-format +msgid "" +"\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" +"\n" +"\n" +"Puoi eliminare il tuo account da questo servizio clicca " +"qui" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" +"\n" +"Grazie per esserti registrato in Taiga\n" +"\n" +"\n" +"\n" +"Speriamo ti piaccia!\n" +"\n" +"\n" +"\n" +"Abbiamo progettato Taiga perché volevamo uno strumento di gestione dei " +"progetti che rimanesse aperto sui nostri computer tutto il giorno, per " +"ricordarci perché amiamo collaborare programmare e progettare.\n" +"\n" +"\n" +"\n" +"Lo abbiamo costruito bello, elegante, semplice e divertente da usare - senza " +"tralasciare flessibilità e potenza.\n" +"\n" +"\n" +"\n" +"--\n" +"\n" +"Il Team di Taiga\n" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" +"\n" +"\n" +"Puoi eliminare il tuo account da questo servizio: %(url)s\n" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "Sei stato Taigazzato!" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "Non c'è nessuno ruolo con questo ID" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" +"Un valore di chiave duplicato viola il vincolo unico. La chiave '{}' esiste " +"già." + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "chiave" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "URL" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "chiave segreta" + +#: taiga/webhooks/models.py:39 +msgid "status code" +msgstr "codice di stato" + +#: taiga/webhooks/models.py:40 +msgid "request data" +msgstr "dati della richiesta" + +#: taiga/webhooks/models.py:41 +msgid "request headers" +msgstr "header della richiesta" + +#: taiga/webhooks/models.py:42 +msgid "response data" +msgstr "dati della risposta" + +#: taiga/webhooks/models.py:43 +msgid "response headers" +msgstr "header della risposta" + +#: taiga/webhooks/models.py:44 +msgid "duration" +msgstr "durata" diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po index 822ec21a..fd7c7502 100644 --- a/taiga/locale/nl/LC_MESSAGES/django.po +++ b/taiga/locale/nl/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: @@ -9,10 +9,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-09 07:47+0000\n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" "Last-Translator: Taiga Dev Team \n" -"Language-Team: Dutch (http://www.transifex.com/projects/p/taiga-back/" +"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -32,40 +32,41 @@ msgstr "ongeldig registratie type" msgid "invalid login type" msgstr "ongeldig login type" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "ongeldige gebruikersnaam" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Verplicht. 255 tekens of minder. Letters, nummers en /./-/_ tekens'" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "Gebruikersnaame is al in gebruik." -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "E-mail adres is al in gebruik." -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "Token stemt niet overeen met een geldige uitnodiging." -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "Gebruiker is al geregistreerd." -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "Lidmaatschap met gebruiker bestaat al." -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "Fout bij het aanmaken van een nieuwe gebruiker." #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "Ongeldig token" @@ -341,12 +342,12 @@ msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten" msgid "Precondition error" msgstr "Preconditie fout" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "Fout in filter params types." -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "'project' moet een integer waarde zijn." @@ -559,32 +560,32 @@ msgstr "fout bij importeren tags" msgid "error importing timelines" msgstr "fout bij importeren tijdlijnen" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" niet gevonden in dit project" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "Ongeldige inhoud. Volgend formaat geldt {\"key\": \"value\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "Het bevat ongeldige eigen velden:" -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "Naam gedupliceerd voor het project" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "Fout bij genereren project dump" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "Fout bij laden project dump" @@ -757,11 +758,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Je project dump is geïmporteerd" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "naam" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "omschrijving" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "volledige naam" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "e-mail adres" @@ -769,12 +820,14 @@ msgstr "e-mail adres" msgid "comment" msgstr "commentaar" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "aanmaakdatum" @@ -843,7 +896,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "De payload is geen geldige json" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "Het project bestaat niet" @@ -851,29 +905,66 @@ msgstr "Het project bestaat niet" msgid "Bad signature" msgstr "Slechte signature" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "De payload is geen geldige application/x-www-form-urlencoded" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "De payload is niet geldig" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "Het element waarnaar verwezen wordt bestaat niet" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "De status bestaat niet" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "Status veranderd door Bitbucket commit" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Ongeldige issue informatie" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Ongeldige issue commentaar informatie" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -881,15 +972,11 @@ msgid "" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." msgstr "" -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "Status veranderd door GitHub commit." -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Ongeldige issue informatie" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -900,15 +987,11 @@ msgid "" "{description}" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "Issue aangemaakt via GitHub." -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "Ongeldige issue commentaar informatie" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -919,7 +1002,7 @@ msgid "" "{message}" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -930,21 +1013,40 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "Status veranderd door GitLab commit" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "Aangemaakt via GitLab" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "Bekijk project" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "Bekijk milestones" @@ -952,240 +1054,232 @@ msgstr "Bekijk milestones" msgid "View user stories" msgstr "Bekijk user stories" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "Bekijk taken" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "Bekijk issues" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "Bekijk wiki pagina's" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "Bekijk wiki links" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "Stem op issues" - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "Vraag lidmaatschap aan" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "Voeg user story toe aan project" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "Voeg commentaar toe aan user stories" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "Voeg commentaar toe aan taken" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "Voeg issues toe" -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "Voeg commentaar toe aan issues" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "Voeg wiki pagina toe" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "Wijzig wiki pagina" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "Voeg wiki link toe" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "Wijzig wiki link" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "Voeg milestone toe" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "Wijzig milestone" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "Verwijder milestone" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "Bekijk user story" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "Voeg user story toe" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "Wijzig user story" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "Verwijder user story" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "Voeg taak toe" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "Wijzig taak" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "Verwijder taak" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "Voeg issue toe" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "Wijzig issue" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "Verwijder issue" -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "Verwijder wiki pagina" -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "Verwijder wiki link" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "Wijzig project" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "Voeg lid toe" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "Verwijder lid" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "Verwijder project" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "Admin project waarden" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "Admin rollen" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "Ongeldige template naam" -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "Ongeldige template omschrijving" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 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:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "Je hebt geen toestamming om dat te bekijken." #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" -msgstr "Niet-partiële updates worden niet ondersteund." +msgid "Partial updates are not supported" +msgstr "" #: taiga/projects/attachments/api.py:62 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:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "eigenaar" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "project" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "inhoud type" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "object id" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "gemodifieerde datum" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "bijgevoegd bestand" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "is verouderd" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "omschrijving" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "volgorde" @@ -1198,33 +1292,44 @@ msgid "Jitsi" msgstr "Jitsi" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "naam" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "type" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "waarden" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "user story" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "taak" -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "issue" @@ -1304,7 +1409,7 @@ msgstr "verwijderd" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "Niet toegewezen" @@ -1351,35 +1456,39 @@ msgstr "Van:" msgid "To:" msgstr "Naar:" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "inhoud" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "geblokkeerde notitie" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "" + +#: taiga/projects/issues/api.py:160 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:143 +#: taiga/projects/issues/api.py:164 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:147 +#: taiga/projects/issues/api.py:168 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:151 +#: taiga/projects/issues/api.py:172 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:155 +#: taiga/projects/issues/api.py:176 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." @@ -1401,10 +1510,6 @@ msgstr "erstniveau" msgid "priority" msgstr "prioriteit" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "type" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1429,10 +1534,23 @@ msgstr "toegewezen aan" msgid "external reference" msgstr "externe referentie" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "slug" @@ -1444,8 +1562,8 @@ msgstr "geschatte start datum" msgid "estimated finish date" msgstr "geschatte datum van afwerking" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "is gesloten" @@ -1474,215 +1592,220 @@ msgstr "'{param}' parameter is verplicht" msgid "'project' parameter is mandatory" msgstr "'project' parameter is verplicht" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "aangemaakt op" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "token" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "uitnodiging extra text" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "gebruiker volgorde" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "The gebruikers is al lid van het project" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "standaard punten" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "standaard US status" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "default taak status" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "standaard prioriteit" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "standaard ernstniveau" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "standaard issue status" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "standaard issue type" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "leden" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "totaal van de milestones" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "totaal story points" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "actief backlog paneel" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "actief kanban paneel" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "actief wiki paneel" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "actief issues paneel" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "videoconference systeem" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" -msgstr "videoconference kamer salt" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "aanmaak template" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "anonieme toestemmingen" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "gebruikers toestemmingen" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "is privé" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "tag kleuren" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "module config" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "is gearchiveerd" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "kleur" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "work in progress limiet" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "waarde" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "standaard rol eigenaar" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "standaard instellingen" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "us statussen" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "punten" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "taak statussen" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "issue statussen" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "issue types" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "prioriteiten" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "ernstniveaus" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "rollen" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "Niet volgend" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "Volgend" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "Negerend" +msgid "None" +msgstr "" -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "volgers" - -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "created date time" msgstr "aanmaak datum en tijd" -#: taiga/projects/notifications/models.py:61 +#: taiga/projects/notifications/models.py:63 msgid "updated date time" msgstr "gewijzigde datum en tijd" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "geschiedenis items" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "verwittig gebruikers" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Verwittiging bestaat voor gespecifieerde gebruiker en project" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2184,7 +2307,7 @@ msgstr "" "\n" "[%(project)s] Wiki Pagina verwijderd \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "Volgers bevat ongeldige gebruikers" @@ -2208,66 +2331,69 @@ msgstr "versie" 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" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "E-mail adres is al in gebruik" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "Ongeldige rol voor project" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "Totaal milestones moet groter of gelijk zijn aan 0" - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "Standaard opties" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "Status van User story" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "Punten" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "Statussen van taken" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "Statussen van Issues" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "Types van issue" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "Prioriteiten" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "Ernstniveaus" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "Rollen" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "Toekomstige sprint" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "Project einde" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." -msgstr "Je hebt geen toestemming om deze taak toe te voegen/te wijzigen" +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "" + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." +msgstr "" #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -2649,14 +2775,18 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:174 -#, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "" + +#: taiga/projects/userstories/api.py:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" msgstr "" -"User story wordt gegenereerd [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -2704,34 +2834,34 @@ msgid "There's no task status with that id" msgstr "Er is geen taak status met dat id" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "Stemmen" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "stemmen" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "Stem" -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "'inhoud' parameter is verplicht" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parameter is verplicht" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "gebruiker met laatste wijziging" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "Persoonlijke info" @@ -2744,64 +2874,64 @@ msgstr "Toestemmingen" msgid "Important dates" msgstr "Belangrijke data" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "Ongeldige gebruikersnaam of e-mail" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "Mail met succes verzonden!" - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "Token is ongeldig" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "Huidig wachtwoord parameter vereist" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "Nieuw wachtwoord parameter vereist" - -#: taiga/users/api.py:184 -msgid "Invalid password length at least 6 charaters needed" -msgstr "Ongeldige lengte van wachtwoord, minstens 6 tekens vereist" - -#: taiga/users/api.py:187 -msgid "Invalid current password" -msgstr "Ongeldig huidig wachtwoord" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "Onvolledige argumenten" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "Ongeldig afbeelding formaat" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "Gedupliceerde e-mail" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "Ongeldige e-mail" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Ongeldige gebruikersnaam of e-mail" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "Mail met succes verzonden!" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Token is ongeldig" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Huidig wachtwoord parameter vereist" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Nieuw wachtwoord parameter vereist" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Ongeldige lengte van wachtwoord, minstens 6 tekens vereist" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "Ongeldig huidig wachtwoord" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Onvolledige argumenten" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Ongeldig afbeelding formaat" + +#: taiga/users/api.py:256 taiga/users/api.py:262 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:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "Ongeldig, weet je zeker dat het token correct is?" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "superuser status" -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -2809,24 +2939,24 @@ msgstr "" "Beduidt dat deze gebruik alle toestemmingen heeft zonder deze expliciet toe " "te wijzen." -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "gebruikersnaam" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Vereist. 30 of minder karakters. Letters, nummers en /./-/_ karakters" -#: taiga/users/models.py:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "Geef een geldige gebruikersnaam in" -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "actief" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2834,55 +2964,55 @@ msgstr "" "Beduidt of deze gebruiker als actief moet behandeld worden. Deselecteer dit " "i.p.v. accounts te verwijderen." -#: taiga/users/models.py:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "biografie" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "foto" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "toetrededatum" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "standaard taal" -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "standaard tijdzone" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "kleur tags" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "e-mail token" -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "nieuw e-mail adres" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "toestemmingen" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "ongeldig" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "Ongeldige gebruikersnaam. Probeer met een andere." -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker." diff --git a/taiga/locale/permissions.py b/taiga/locale/permissions.py index 406a4285..1c14c352 100644 --- a/taiga/locale/permissions.py +++ b/taiga/locale/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 00000000..ba47a433 --- /dev/null +++ b/taiga/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,3621 @@ +# taiga-back.taiga. +# Copyright (C) 2014-2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +# David Barragán , 2015 +# Wiktor Żurawik , 2015 +# Wojtek Jurkowlaniec , 2015 +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/" +"language/pl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: taiga/auth/api.py:99 +msgid "Public register is disabled." +msgstr "Publiczna rejestracja jest wyłączona" + +#: taiga/auth/api.py:132 +msgid "invalid register type" +msgstr "Nieprawidłowy typ rejestracji" + +#: taiga/auth/api.py:145 +msgid "invalid login type" +msgstr "Nieprawidłowy typ logowania" + +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 +msgid "invalid username" +msgstr "Nieprawidłowa nazwa użytkownika" + +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "Wymagane. Maksymalnie 255 znaków. Litery, cyfry oraz /./-/_ " + +#: taiga/auth/services.py:73 +msgid "Username is already in use." +msgstr "Nazwa użytkownika jest już używana." + +#: taiga/auth/services.py:76 +msgid "Email is already in use." +msgstr "Ten adres email jest już w użyciu." + +#: taiga/auth/services.py:92 +msgid "Token not matches any valid invitation." +msgstr "Token nie zgadza się z żadnym zaproszeniem" + +#: taiga/auth/services.py:120 +msgid "User is already registered." +msgstr "Użytkownik już zarejestrowany" + +#: taiga/auth/services.py:144 +msgid "Membership with user is already exists." +msgstr "Członkowstwo z użytkownikiem już istnieje." + +#: taiga/auth/services.py:170 +msgid "Error on creating new user." +msgstr "Błąd przy tworzeniu użytkownika." + +#: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 +msgid "Invalid token" +msgstr "Nieprawidłowy token" + +#: taiga/base/api/fields.py:268 +msgid "This field is required." +msgstr "To pole jest wymagane." + +#: taiga/base/api/fields.py:269 taiga/base/api/relations.py:311 +msgid "Invalid value." +msgstr "Nieprawidłowa wartość." + +#: taiga/base/api/fields.py:453 +#, python-format +msgid "'%s' value must be either True or False." +msgstr "'%s' wartość musi przyjąć True albo False," + +#: taiga/base/api/fields.py:517 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "" +"Podaj prawidłowy 'slug' zawierający litery, cyfry, podkreślenia lub myślniki." + +#: taiga/base/api/fields.py:532 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" +"Dokonał właściwego wyboru. Wartość %(value)s nie jest jedną z dostępnych " +"opcji." + +#: taiga/base/api/fields.py:595 +msgid "Enter a valid email address." +msgstr "Podaj właściwy adres email." + +#: taiga/base/api/fields.py:637 +#, python-format +msgid "Date has wrong format. Use one of these formats instead: %s" +msgstr "Zły format. Użyj jednego z tych formatów: %s" + +#: taiga/base/api/fields.py:701 +#, python-format +msgid "Datetime has wrong format. Use one of these formats instead: %s" +msgstr "Zły format. Użyj jednego z tych formatów: %s" + +#: taiga/base/api/fields.py:771 +#, python-format +msgid "Time has wrong format. Use one of these formats instead: %s" +msgstr "Zły format. Użyj jednego z tych formatów: %s" + +#: taiga/base/api/fields.py:828 +msgid "Enter a whole number." +msgstr "Wpisz cały numer" + +#: taiga/base/api/fields.py:829 taiga/base/api/fields.py:882 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "Upewnij się, że wartość jest mniejsza lub równa od %(limit_value)s." + +#: taiga/base/api/fields.py:830 taiga/base/api/fields.py:883 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "Upewnij się, że wartość jest większa lub równa od %(limit_value)s." + +#: taiga/base/api/fields.py:860 +#, python-format +msgid "\"%s\" value must be a float." +msgstr "\"%s\" wartość musi być zmiennoprzecinkowa." + +#: taiga/base/api/fields.py:881 +msgid "Enter a number." +msgstr "Wpisz numer." + +#: taiga/base/api/fields.py:884 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Upewnij się że nie podałeś więcej niż %s znaków." + +#: taiga/base/api/fields.py:885 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Upewnij się, że nie ma więcej niż %s miejsc po przecinku." + +#: taiga/base/api/fields.py:886 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Upewnij się, że nie ma więcej niż %s cyfr przed przecinkiem." + +#: taiga/base/api/fields.py:953 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "Plik nie został wysłany. Sprawdź kodowanie znaków w formularzu." + +#: taiga/base/api/fields.py:954 +msgid "No file was submitted." +msgstr "Plik nie został wysłany." + +#: taiga/base/api/fields.py:955 +msgid "The submitted file is empty." +msgstr "Wysłany plik jest pusty." + +#: taiga/base/api/fields.py:956 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "" +"Upewnij się, że nazwa pliku ma maksymalnie %(max)d znaków.(Ilość znaków to: " +"%(length)d)." + +#: taiga/base/api/fields.py:957 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "Proszę wybrać jedną z opcji, nie obie." + +#: taiga/base/api/fields.py:997 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Prześlij właściwy obraz. Plik który próbujesz przesłać nie jest obrazem lub " +"jest uszkodzony." + +#: taiga/base/api/pagination.py:115 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "Strona nie jest ostatnią i nie może zostać zmieniona na int." + +#: taiga/base/api/pagination.py:119 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Niewłaściwa strona (%(page_number)s): %(message)s" + +#: taiga/base/api/permissions.py:61 +msgid "Invalid permission definition." +msgstr "Nieprawidłowa definicja uprawnień." + +#: taiga/base/api/relations.py:221 +#, python-format +msgid "Invalid pk '%s' - object does not exist." +msgstr "Nieprawidłowa wartość klucza '%s' -Obiekt nie istniej." + +#: taiga/base/api/relations.py:222 +#, python-format +msgid "Incorrect type. Expected pk value, received %s." +msgstr "Niepoprawny typ. Oczekiwana wartość, otrzymana %s." + +#: taiga/base/api/relations.py:310 +#, python-format +msgid "Object with %s=%s does not exist." +msgstr "Obiekt z %s=%s nie istnieje." + +#: taiga/base/api/relations.py:346 +msgid "Invalid hyperlink - No URL match" +msgstr "Nieprawidłowy odnośnik - brak pasującego URL" + +#: taiga/base/api/relations.py:347 +msgid "Invalid hyperlink - Incorrect URL match" +msgstr "Nieprawidłowy odnośnik - źle dopasowany URL" + +#: taiga/base/api/relations.py:348 +msgid "Invalid hyperlink due to configuration error" +msgstr "Nieprawidłowy odnośnik z powodu błędu konfiguracji" + +#: taiga/base/api/relations.py:349 +msgid "Invalid hyperlink - object does not exist." +msgstr "Nieprawidłowy odnośnik - obiekt nie istnieje." + +#: taiga/base/api/relations.py:350 +#, python-format +msgid "Incorrect type. Expected url string, received %s." +msgstr "Niepoprawny typ. Oczekiwany url, otrzymany %s." + +#: taiga/base/api/serializers.py:296 +msgid "Invalid data" +msgstr "Nieprawidłowa dana" + +#: taiga/base/api/serializers.py:388 +msgid "No input provided" +msgstr "Nic nie wpisano" + +#: taiga/base/api/serializers.py:548 +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:559 +msgid "Expected a list of items." +msgstr "Oczekiwana lista elementów." + +#: taiga/base/api/views.py:100 +msgid "Not found" +msgstr "Nie znaleziono" + +#: taiga/base/api/views.py:103 +msgid "Permission denied" +msgstr "Dostęp zabroniony" + +#: taiga/base/api/views.py:451 +msgid "Server application error" +msgstr "Błąd aplikacji serwera" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "Błąd połączenia." + +#: taiga/base/exceptions.py:53 +msgid "Malformed request." +msgstr "Błędne żądanie." + +#: taiga/base/exceptions.py:58 +msgid "Incorrect authentication credentials." +msgstr "Nieprawidłowe dane uwierzytelniające." + +#: taiga/base/exceptions.py:63 +msgid "Authentication credentials were not provided." +msgstr "Nie podano danych uwierzytelniających." + +#: taiga/base/exceptions.py:68 +msgid "You do not have permission to perform this action." +msgstr "Nie masz uprawnień do wykonania tej czynności." + +#: taiga/base/exceptions.py:73 +#, python-format +msgid "Method '%s' not allowed." +msgstr "Metoda %s nie dozwolona." + +#: taiga/base/exceptions.py:81 +msgid "Could not satisfy the request's Accept header" +msgstr "Nie udało się spełnić żądania Accept Header" + +#: taiga/base/exceptions.py:90 +#, python-format +msgid "Unsupported media type '%s' in request." +msgstr "Niewspierany typ pliku '%s' w żądaniu." + +#: taiga/base/exceptions.py:98 +msgid "Request was throttled." +msgstr "Żądanie zostało zduszone." + +#: taiga/base/exceptions.py:99 +#, python-format +msgid "Expected available in %d second%s." +msgstr "Oczekiwana dostępność w ciągu %d sekund%s." + +#: taiga/base/exceptions.py:113 +msgid "Unexpected error" +msgstr "Nieoczekiwany błąd" + +#: taiga/base/exceptions.py:125 +msgid "Not found." +msgstr "Nie odnaleziono." + +#: taiga/base/exceptions.py:130 +msgid "Method not supported for this endpoint." +msgstr "Metoda nie wspierana dla tej końcówki." + +#: taiga/base/exceptions.py:138 taiga/base/exceptions.py:146 +msgid "Wrong arguments." +msgstr "Złe argumenty." + +#: taiga/base/exceptions.py:150 +msgid "Data validation error" +msgstr "Błąd walidacji dancyh" + +#: taiga/base/exceptions.py:162 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "Błąd integralności dla błędnych lub nieprawidłowych argumentów" + +#: taiga/base/exceptions.py:169 +msgid "Precondition error" +msgstr "Błąd warunków wstępnych" + +#: taiga/base/filters.py:80 +msgid "Error in filter params types." +msgstr "Błąd w parametrach typów filtrów." + +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 +msgid "'project' must be an integer value." +msgstr "'project' musi być wartością typu int." + +#: taiga/base/tags.py:25 +msgid "tags" +msgstr "tagi" + +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "Taiga" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "Obserwuj nas na Twitterze" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "Pobierz kog z GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "Odwiedź naszą stronę www" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "Taiga.io" + +#: taiga/base/templates/emails/base-body-html.jinja:423 +#: taiga/base/templates/emails/hero-body-html.jinja:397 +#: taiga/base/templates/emails/updates-body-html.jinja:459 +#, python-format +msgid "" +"\n" +" Taiga Support:\n" +" %(support_url)s\n" +"
\n" +" Contact us:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Mailing list:\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " +msgstr "" +"\n" +" Pomoc Taiga:\n" +" %(support_url)s\n" +"
\n" +" Skontaktuj się z " +"nami:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Lista mailingowa:" +"\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Zostałeś zaTaigowany :)" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

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

Zostałeś zataigowany!\n" +"

Witaj w Taiga, " +"otwartoźródłowym, zwinnym narzędziu do zarządzania

\n" +" " + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "[Taiga] Aktualizacje" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "Aktualizacje" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

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

komentarz:" +"

\n" +"

" +"%(comment)s

\n" +" " + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" +"\n" +" Komentarz: %(comment)s\n" +" " + +#: taiga/export_import/api.py:103 +msgid "We needed at least one role" +msgstr "Potrzeba conajmiej jednej roli" + +#: taiga/export_import/api.py:197 +msgid "Needed dump file" +msgstr "Wymagany plik zrzutu" + +#: taiga/export_import/api.py:204 +msgid "Invalid dump format" +msgstr "Nieprawidłowy format zrzutu" + +#: taiga/export_import/dump_service.py:96 +msgid "error importing project data" +msgstr "błąd w trakcie importu danych projektu" + +#: taiga/export_import/dump_service.py:109 +msgid "error importing lists of project attributes" +msgstr "błąd w trakcie importu atrybutów projektu" + +#: taiga/export_import/dump_service.py:114 +msgid "error importing default project attributes values" +msgstr "błąd w trakcie importu domyślnych atrybutów projektu" + +#: taiga/export_import/dump_service.py:124 +msgid "error importing custom attributes" +msgstr "błąd w trakcie importu niestandardowych atrybutów" + +#: taiga/export_import/dump_service.py:129 +msgid "error importing roles" +msgstr "błąd w trakcie importu ról" + +#: taiga/export_import/dump_service.py:144 +msgid "error importing memberships" +msgstr "błąd w trakcie importu członkostw" + +#: taiga/export_import/dump_service.py:149 +msgid "error importing sprints" +msgstr "błąd w trakcie importu sprintów" + +#: taiga/export_import/dump_service.py:154 +msgid "error importing wiki pages" +msgstr "błąd w trakcie importu stron Wiki" + +#: taiga/export_import/dump_service.py:159 +msgid "error importing wiki links" +msgstr "błąd w trakcie importu linków Wiki" + +#: taiga/export_import/dump_service.py:164 +msgid "error importing issues" +msgstr "błąd w trakcie importu zgłoszeń" + +#: taiga/export_import/dump_service.py:169 +msgid "error importing user stories" +msgstr "błąd w trakcie importu historyjek użytkownika" + +#: taiga/export_import/dump_service.py:174 +msgid "error importing tasks" +msgstr "błąd w trakcie importu zadań" + +#: taiga/export_import/dump_service.py:179 +msgid "error importing tags" +msgstr "błąd w trakcie importu tagów" + +#: taiga/export_import/dump_service.py:183 +msgid "error importing timelines" +msgstr "błąd w trakcie importu osi czasu" + +#: taiga/export_import/serializers.py:163 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" nie odnaleziono w projekcie" + +#: taiga/export_import/serializers.py:428 +#: taiga/projects/custom_attributes/serializers.py:103 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Niewłaściwa zawartość. Musi to być {\"key\": \"value\",...}" + +#: taiga/export_import/serializers.py:443 +#: taiga/projects/custom_attributes/serializers.py:118 +msgid "It contain invalid custom fields." +msgstr "Zawiera niewłaściwe pola niestandardowe." + +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 +msgid "Name duplicated for the project" +msgstr "Nazwa projektu zduplikowana" + +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 +msgid "Error generating project dump" +msgstr "Błąd w trakcie generowania zrzutu projektu" + +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 +msgid "Error loading project dump" +msgstr "Błąd w trakcie wczytywania zrzutu projektu" + +#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump generated

\n" +"

Hello %(user)s,

\n" +"

Your dump from project %(project)s has been correctly generated.\n" +"

You can download it here:

\n" +" Download the dump file\n" +"

This file will be deleted on %(deletion_date)s.

\n" +"

The Taiga Team

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

Zrzut projektu wygenerowany

\n" +"

Witaj %(user)s,

\n" +"

Twój zrzut projektu %(project)s został wygenerowany prawidłowo.\n" +"

Możesz go pobrać tutaj:

\n" +" Pobierz plik zrzutu\n" +"

Ten plik zostanie usunięty dnia %(deletion_date)s.

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/export_import/templates/emails/dump_project-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your dump from project %(project)s has been correctly generated. You can " +"download it here:\n" +"\n" +"%(url)s\n" +"\n" +"This file will be deleted on %(deletion_date)s.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Witaj %(user)s,\n" +"\n" +"Twój zrzut z projektu %(project)s został wygenerowany prawidłowo. Możesz " +"pobrać go tutaj:\n" +"\n" +"%(url)s\n" +"\n" +"Plik zostanie usunięty dnia: %(deletion_date)s.\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "[%(project)s] Twój plik zrzutu został wygenerowany" + +#: taiga/export_import/templates/emails/export_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project %(project)s has not been exported correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"

Witaj %(user)s,

\n" +"

Twój projekt %(project)s Nie został wyeksportowany prawidłowo.

\n" +"

Administrator Taiga został o tym poinformowany.
Proszę spróbuj " +"ponownie lub skontaktuj się z administratorem lub wsparciem Taiga\n" +" %(support_email)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/export_import/templates/emails/export_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"Your project %(project)s has not been exported correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Witaj %(user)s,\n" +"\n" +"%(error_message)s\n" +"Twój projekt%(project)s nie został wyeksportowany prawidłowo.\n" +"\n" +"Administrator Taiga został o tym poinformowany.\n" +"\n" +"Proszę spróbuj ponownie lub skontaktuj się z administratorem lub wsparciem " +"Taiga %(support_email)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "[%(project)s] %(error_subject)s" + +#: taiga/export_import/templates/emails/import_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project has not been importer correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"

Witaj %(user)s,

\n" +"

Twój projekt nie został zaimportowany prawidłowo.

\n" +"

Administrator Taiga został o tym poinformowany.
Proszę spróbuj " +"ponownie lub skontaktuj się z administratorem lub wsparciem Taiga\n" +" %(support_email)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/export_import/templates/emails/import_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Your project has not been importer correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Witaj %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Twój projekt nie został zaimportowany prawidłowo.\n" +"\n" +"Administrator Taiga został o tym poinformowany.\n" +"\n" +"roszę spróbuj ponownie lub skontaktuj się z administratorem lub wsparciem " +"Taiga %(support_email)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "[Taiga] %(error_subject)s" + +#: taiga/export_import/templates/emails/load_dump-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump imported

\n" +"

Hello %(user)s,

\n" +"

Your project dump has been correctly imported.

\n" +" Go to %(project)s\n" +"

The Taiga Team

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

Zrzut projektu zaimportowany

\n" +"

Witaj %(user)s,

\n" +"

Twój zrzut projektu został prawidłowo zaimportowany.

\n" +" Idź do %(project)s\n" +"

Zespół Taiga

\n" +" " + +#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your project dump has been correctly imported.\n" +"\n" +"You can see the project %(project)s here:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Witaj %(user)s,\n" +"\n" +"Twój zrzut projektu został prawidłowo zaimportowany.\n" +"\n" +"Możesz zobaczyć projekt %(project)s tutaj:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "[%(project)s] Twój zrzut projektu został prawidłowo zaimportowany" + +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "nazwa" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "web" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "opis" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "Następny url" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "użytkownik" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "aplikacja" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 +msgid "full name" +msgstr "Imię i Nazwisko" + +#: taiga/feedback/models.py:25 taiga/users/models.py:108 +msgid "email address" +msgstr "adres e-mail" + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "komentarz" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 +msgid "created date" +msgstr "data utworzenia" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

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

Feedback

\n" +"

Taiga otrzymała informacje od %(full_name)s <%(email)s>

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

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

Komentarz

\n" +"

%(comment)s

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "Dodatkowe info" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" +"---------\n" +"- Od: %(full_name)s <%(email)s>\n" +"---------\n" +"- Komentarz:\n" +"%(comment)s\n" +"---------" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "- Dodatkowe info:" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" +"\n" +"[Taiga] Informacje od %(full_name)s <%(email)s>\n" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "Źródło nie jest prawidłowym plikiem json" + +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 +msgid "The project doesn't exist" +msgstr "Projekt nie istnieje" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "Błędna sygnatura" + +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 +msgid "The referenced element doesn't exist" +msgstr "Element referencyjny nie istnieje" + +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 +msgid "The status doesn't exist" +msgstr "Status nie istnieje" + +#: taiga/hooks/bitbucket/event_hooks.py:97 +msgid "Status changed from BitBucket commit" +msgstr "Status zmieniony przez commit z BitBucket" + +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Nieprawidłowa informacja o zgłoszeniu" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Zgłoszenie utworzone przez [@{bitbucket_user_name}]({bitbucket_user_url} " +"\"Zobacz profil użytkownika @{bitbucket_user_name}'s \") na BitBucket.\n" +"Źródłowe zgłoszenie z BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Idź do 'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "Zgłoszenie utworzone przez BitBucket." + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Nieprawidłowa informacja o komentarzu do zgłoszenia" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Skomentowane przez [@{bitbucket_user_name}]({bitbucket_user_url} \"Zobacz " +"profil użytkownika @{bitbucket_user_name}'s\") na BitBucket.\n" +"Źródłowe zgłoszenie z BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Idź do 'bb#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"Komentarz z BitBucket:\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 +#, python-brace-format +msgid "" +"Status changed by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +msgstr "" +"Status zmieniony przez [@{github_user_name}]({github_user_url} \"Zobacz " +"profil użytkownika @{github_user_name}'s \") z commitu na GitHub " +"[{commit_id}]({commit_url} \"Zobacz commit'{commit_id} - " +"{commit_message}'\")." + +#: taiga/hooks/github/event_hooks.py:107 +msgid "Status changed from GitHub commit." +msgstr "Status zmieniony przez commit z GitHub" + +#: taiga/hooks/github/event_hooks.py:157 +#, python-brace-format +msgid "" +"Issue created by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Zgłoszenie utworzone przez [@{github_user_name}]({github_user_url} \"Zobacz " +"profil użytkownika @{github_user_name}'s \") na GitHub.\n" +"Źródłowe zgłoszenie z GitHub: [gh#{number} - {subject}]({github_url} \"Idź " +"do 'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/github/event_hooks.py:168 +msgid "Issue created from GitHub." +msgstr "Zgłoszenie utworzone przez GitHub." + +#: taiga/hooks/github/event_hooks.py:200 +#, python-brace-format +msgid "" +"Comment by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Skomentowane przez [@{github_user_name}]({github_user_url} \"Zobacz profil " +"użytkownika @{github_user_name}'s GitHub profile\") na GitHub.\n" +"Źródłowe zgłoszenie z GitHub: [gh#{number} - {subject}]({github_url} \"Idź " +"do 'gh#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:211 +#, python-brace-format +msgid "" +"Comment From GitHub:\n" +"\n" +"{message}" +msgstr "" +"Komentarz z GitHub:\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:86 +msgid "Status changed from GitLab commit" +msgstr "Status zmieniony przez commit z GitLab" + +#: taiga/hooks/gitlab/event_hooks.py:128 +msgid "Created from GitLab" +msgstr "Utworzone przez GitLab" + +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Skomentowane przez [@{gitlab_user_name}]({gitlab_user_url} \"Zobacz profil " +"użytkownika @{gitlab_user_name}'s \") na GitLab.\n" +"Źródłowe zgłoszenie z: [gl#{number} - {subject}]({gitlab_url} \"Idź do " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"Komentarz z GitLab:\n" +"\n" +"{message}" + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:51 +msgid "View project" +msgstr "Zobacz projekt" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:53 +msgid "View milestones" +msgstr "Zobacz kamienie milowe" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "Zobacz historyjki użytkownika" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 +msgid "View tasks" +msgstr "Zobacz zadania" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:68 +msgid "View issues" +msgstr "Zobacz zgłoszenia" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 +msgid "View wiki pages" +msgstr "Zobacz strony Wiki" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 +msgid "View wiki links" +msgstr "Zobacz linki Wiki" + +#: taiga/permissions/permissions.py:38 +msgid "Request membership" +msgstr "Poproś o członkowstwo" + +#: taiga/permissions/permissions.py:39 +msgid "Add user story to project" +msgstr "Dodaj historyjkę użytkownika do projektu" + +#: taiga/permissions/permissions.py:40 +msgid "Add comments to user stories" +msgstr "Dodaj komentarze do historyjek użytkownika" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to tasks" +msgstr "Dodaj komentarze do zadań" + +#: taiga/permissions/permissions.py:42 +msgid "Add issues" +msgstr "Dodaj zgłoszenia" + +#: taiga/permissions/permissions.py:43 +msgid "Add comments to issues" +msgstr "Dodaj komentarze do zgłoszeń" + +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 +msgid "Add wiki page" +msgstr "Dodaj strony Wiki" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 +msgid "Modify wiki page" +msgstr "Modyfikuj stronę Wiki" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 +msgid "Add wiki link" +msgstr "Dodaj link do Wiki" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 +msgid "Modify wiki link" +msgstr "Modyfikuj link do Wiki" + +#: taiga/permissions/permissions.py:54 +msgid "Add milestone" +msgstr "Dodaj kamień milowy" + +#: taiga/permissions/permissions.py:55 +msgid "Modify milestone" +msgstr "Modyfikuj Kamień milowy" + +#: taiga/permissions/permissions.py:56 +msgid "Delete milestone" +msgstr "Usuń kamień milowy" + +#: taiga/permissions/permissions.py:58 +msgid "View user story" +msgstr "Zobacz historyjkę użytkownika" + +#: taiga/permissions/permissions.py:59 +msgid "Add user story" +msgstr "Dodaj historyjkę użytkownika" + +#: taiga/permissions/permissions.py:60 +msgid "Modify user story" +msgstr "Modyfikuj historyjkę użytkownika" + +#: taiga/permissions/permissions.py:61 +msgid "Delete user story" +msgstr "Usuń historyjkę użytkownika" + +#: taiga/permissions/permissions.py:64 +msgid "Add task" +msgstr "Dodaj zadanie" + +#: taiga/permissions/permissions.py:65 +msgid "Modify task" +msgstr "Modyfikuj zadanie" + +#: taiga/permissions/permissions.py:66 +msgid "Delete task" +msgstr "Usuń zadanie" + +#: taiga/permissions/permissions.py:69 +msgid "Add issue" +msgstr "Dodaj zgłoszenie" + +#: taiga/permissions/permissions.py:70 +msgid "Modify issue" +msgstr "Modyfikuj zgłoszenie" + +#: taiga/permissions/permissions.py:71 +msgid "Delete issue" +msgstr "Usuń zgłoszenie" + +#: taiga/permissions/permissions.py:76 +msgid "Delete wiki page" +msgstr "Usuń stronę Wiki" + +#: taiga/permissions/permissions.py:81 +msgid "Delete wiki link" +msgstr "Usuń link Wiki" + +#: taiga/permissions/permissions.py:85 +msgid "Modify project" +msgstr "Modyfikuj projekt" + +#: taiga/permissions/permissions.py:86 +msgid "Add member" +msgstr "Dodaj członka zespołu" + +#: taiga/permissions/permissions.py:87 +msgid "Remove member" +msgstr "Usuń członka zespołu" + +#: taiga/permissions/permissions.py:88 +msgid "Delete project" +msgstr "Usuń projekt" + +#: taiga/permissions/permissions.py:89 +msgid "Admin project values" +msgstr "Administruj wartościami projektu" + +#: taiga/permissions/permissions.py:90 +msgid "Admin roles" +msgstr "Administruj rolami" + +#: taiga/projects/api.py:202 +msgid "Not valid template name" +msgstr "Nieprawidłowa nazwa szablonu" + +#: taiga/projects/api.py:205 +msgid "Not valid template description" +msgstr "Nieprawidłowy opis szablonu" + +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 +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:511 +msgid "You don't have permisions to see that." +msgstr "Nie masz uprawnień by to zobaczyć." + +#: taiga/projects/attachments/api.py:47 +msgid "Partial updates are not supported" +msgstr "" + +#: taiga/projects/attachments/api.py:62 +msgid "Project ID not matches between object and project" +msgstr "ID nie pasuje pomiędzy obiektem a projektem" + +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 +#: taiga/userstorage/models.py:25 +msgid "owner" +msgstr "właściciel" + +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 +msgid "project" +msgstr "projekt" + +#: taiga/projects/attachments/models.py:56 +msgid "content type" +msgstr "typ zawartości" + +#: taiga/projects/attachments/models.py:58 +msgid "object id" +msgstr "id obiektu" + +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "data modyfikacji" + +#: taiga/projects/attachments/models.py:69 +msgid "attached file" +msgstr "załączony plik" + +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 +msgid "is deprecated" +msgstr "jest przestarzałe" + +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 +msgid "order" +msgstr "kolejność" + +#: taiga/projects/choices.py:21 +msgid "AppearIn" +msgstr "AppearIn" + +#: taiga/projects/choices.py:22 +msgid "Jitsi" +msgstr "Jitsi" + +#: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "Niestandardowy" + +#: taiga/projects/choices.py:24 +msgid "Talky" +msgstr "Talky" + +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Tekst" + +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Teks wielowierszowy" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "typ" + +#: taiga/projects/custom_attributes/models.py:87 +msgid "values" +msgstr "wartości" + +#: taiga/projects/custom_attributes/models.py:97 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 +msgid "user story" +msgstr "historyjka użytkownika" + +#: taiga/projects/custom_attributes/models.py:112 +msgid "task" +msgstr "zadanie" + +#: taiga/projects/custom_attributes/models.py:127 +msgid "issue" +msgstr "zgłoszenie" + +#: taiga/projects/custom_attributes/serializers.py:57 +msgid "Already exists one with the same name." +msgstr "Już istnieje jeden z taką nazwą." + +#: taiga/projects/history/api.py:70 +msgid "Comment already deleted" +msgstr "Komentarz został już usunięty" + +#: taiga/projects/history/api.py:89 +msgid "Comment not deleted" +msgstr "Komentarz nie został usunięty" + +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "Zmień" + +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "Utwórz" + +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "Usuń" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "%(role)s punkty roli" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:193 +msgid "from" +msgstr "od" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:199 +msgid "to" +msgstr "do" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "Dodano nowy załącznik" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "Zaktualizowany załącznik" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "przestarzałe" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "nie przestarzałe" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "Usuń załącznik" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "dodane" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "usuniete" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 +msgid "Unassigned" +msgstr "Nieprzypisane" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:211 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "-usunięte-" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "do:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "od:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "Dodane" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "Zmienione" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "Usunięte" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "dodane:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "usunięte:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "Od:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "Do:" + +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 +msgid "content" +msgstr "zawartość" + +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "zaglokowana notatka" + +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "sprint" + +#: taiga/projects/issues/api.py:160 +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:164 +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:168 +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:172 +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:176 +msgid "You don't have permissions to set this type to this issue." +msgstr "Nie masz uprawnień do ustawienia typu dla tego zgłoszenia." + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "ref" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "status" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "ważność" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "priorytet" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "kamień milowy" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "data zakończenia" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "temat" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "przypisane do" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "źródło zgłoszenia" + +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "ilość" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 +msgid "slug" +msgstr "slug" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "szacowana data rozpoczecia" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "szacowana data zakończenia" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 +msgid "is closed" +msgstr "jest zamknięte" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "dostępność" + +#: taiga/projects/milestones/models.py:75 +msgid "The estimated start must be previous to the estimated finish." +msgstr "Szacowana data rozpoczęcia musi być wcześniejsza niż data zakończenia." + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "Nie ma sprintu o takim ID" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "jest zablokowane" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "'{param}' parameter is mandatory" +msgstr "'{param}' parametr jest obowiązkowy" + +#: taiga/projects/mixins/ordering.py:51 +msgid "'project' parameter is mandatory" +msgstr "'project' parametr jest obowiązkowy" + +#: taiga/projects/models.py:66 +msgid "email" +msgstr "e-mail" + +#: taiga/projects/models.py:68 +msgid "create at" +msgstr "utwórz na" + +#: taiga/projects/models.py:70 taiga/users/models.py:130 +msgid "token" +msgstr "token" + +#: taiga/projects/models.py:76 +msgid "invitation extra text" +msgstr "dodatkowy tekst w zaproszeniu" + +#: taiga/projects/models.py:79 +msgid "user order" +msgstr "kolejność użytkowników" + +#: taiga/projects/models.py:89 +msgid "The user is already member of the project" +msgstr "Użytkownik już jest członkiem tego projektu" + +#: taiga/projects/models.py:104 +msgid "default points" +msgstr "domyślne punkty" + +#: taiga/projects/models.py:108 +msgid "default US status" +msgstr "domyślny status dla HU" + +#: taiga/projects/models.py:112 +msgid "default task status" +msgstr "domyślny status dla zadania" + +#: taiga/projects/models.py:115 +msgid "default priority" +msgstr "domyślny priorytet" + +#: taiga/projects/models.py:118 +msgid "default severity" +msgstr "domyślna ważność" + +#: taiga/projects/models.py:122 +msgid "default issue status" +msgstr "domyślny status dla zgłoszenia" + +#: taiga/projects/models.py:126 +msgid "default issue type" +msgstr "domyślny typ dla zgłoszenia" + +#: taiga/projects/models.py:147 +msgid "members" +msgstr "członkowie" + +#: taiga/projects/models.py:150 +msgid "total of milestones" +msgstr "wszystkich kamieni milowych" + +#: taiga/projects/models.py:151 +msgid "total story points" +msgstr "wszystkich punktów " + +#: taiga/projects/models.py:154 taiga/projects/models.py:614 +msgid "active backlog panel" +msgstr "aktywny panel backlog" + +#: taiga/projects/models.py:156 taiga/projects/models.py:616 +msgid "active kanban panel" +msgstr "aktywny panel Kanban" + +#: taiga/projects/models.py:158 taiga/projects/models.py:618 +msgid "active wiki panel" +msgstr "aktywny panel Wiki" + +#: taiga/projects/models.py:160 taiga/projects/models.py:620 +msgid "active issues panel" +msgstr "aktywny panel zgłoszeń " + +#: taiga/projects/models.py:163 taiga/projects/models.py:623 +msgid "videoconference system" +msgstr "system wideokonferencji" + +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "dodatkowe dane dla wideokonferencji" + +#: taiga/projects/models.py:170 +msgid "creation template" +msgstr "szablon " + +#: taiga/projects/models.py:173 +msgid "anonymous permissions" +msgstr "uprawnienia anonimowych" + +#: taiga/projects/models.py:177 +msgid "user permissions" +msgstr "uprawnienia użytkownika" + +#: taiga/projects/models.py:180 +msgid "is private" +msgstr "jest prywatna" + +#: taiga/projects/models.py:191 +msgid "tags colors" +msgstr "kolory tagów" + +#: taiga/projects/models.py:383 +msgid "modules config" +msgstr "konfiguracja modułów" + +#: taiga/projects/models.py:402 +msgid "is archived" +msgstr "zarchiwizowane" + +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 +msgid "color" +msgstr "kolor" + +#: taiga/projects/models.py:406 +msgid "work in progress limit" +msgstr "limit postępu prac" + +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 +msgid "value" +msgstr "wartość" + +#: taiga/projects/models.py:611 +msgid "default owner's role" +msgstr "domyśla rola właściciela" + +#: taiga/projects/models.py:627 +msgid "default options" +msgstr "domyślne opcje" + +#: taiga/projects/models.py:628 +msgid "us statuses" +msgstr "statusy HU" + +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 +msgid "points" +msgstr "pinkty" + +#: taiga/projects/models.py:630 +msgid "task statuses" +msgstr "statusy zadań" + +#: taiga/projects/models.py:631 +msgid "issue statuses" +msgstr "statusy zgłoszeń" + +#: taiga/projects/models.py:632 +msgid "issue types" +msgstr "typy zgłoszeń" + +#: taiga/projects/models.py:633 +msgid "priorities" +msgstr "priorytety" + +#: taiga/projects/models.py:634 +msgid "severities" +msgstr "ważność" + +#: taiga/projects/models.py:635 +msgid "roles" +msgstr "role" + +#: taiga/projects/notifications/choices.py:28 +msgid "Involved" +msgstr "" + +#: taiga/projects/notifications/choices.py:29 +msgid "All" +msgstr "" + +#: taiga/projects/notifications/choices.py:30 +msgid "None" +msgstr "" + +#: taiga/projects/notifications/models.py:61 +msgid "created date time" +msgstr "data utworzenia" + +#: taiga/projects/notifications/models.py:63 +msgid "updated date time" +msgstr "data aktualizacji" + +#: taiga/projects/notifications/models.py:65 +msgid "history entries" +msgstr "wpisy historii" + +#: taiga/projects/notifications/models.py:68 +msgid "notify users" +msgstr "powiadom użytkowników" + +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "Obserwowane" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 +msgid "Notify exists for specified user and project" +msgstr "Powiadomienie istnieje dla określonego użytkownika i projektu" + +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "Nieprawidłowa wartość dla poziomu notyfikacji" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue updated

\n" +"

Hello %(user)s,
%(changer)s has updated an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +" " +msgstr "" +"\n" +"

Zgłoszenie zaktualizowane

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s zaktualizował zgłoszenie " +"w projekcie %(project)s

\n" +"

Zgłoszenie #%(ref)s %(subject)s

\n" +" Zobacz zgłoszenie\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Issue updated\n" +"Hello %(user)s, %(changer)s has updated an issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Zgłoszenie zaktualizowane\n" +"Witaj, użytkownik %(user)s, %(changer)s zaktualizował zgłoszenie w projekcie " +"%(project)s\n" +"Zobacz zgłoszenie #%(ref)s %(subject)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Zaktualizował zgłoszenie #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New issue created

\n" +"

Hello %(user)s,
%(changer)s has created a new issue on " +"%(project)s

\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +"

The Taiga Team

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

Utworzono nowe zgłoszenie

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s utworzył nowe zgłoszenie " +"w projekcie %(project)s

\n" +"

Zgłoszenie #%(ref)s %(subject)s

\n" +" Zobacz zgłoszenie\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New issue created\n" +"Hello %(user)s, %(changer)s has created a new issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Utworzono nowe zgłoszenie\n" +"Witaj, użytkownik %(user)s, %(changer)s utworzył nowe zgłoszenie w projekcie " +"%(project)s\n" +"Zobacz zgłoszenie #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Utworzył zgłoszenie #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Zgłoszenie usunięte

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s usunął zgłoszenie w " +"projekcie %(project)s

\n" +"

Zgłoszenie #%(ref)s %(subject)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Issue deleted\n" +"Hello %(user)s, %(changer)s has deleted an issue on %(project)s\n" +"Issue #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Zgłoszenie usunięte\n" +"Witaj, użytkownik %(user)s, %(changer)s usunął zgłoszenie w projekcie " +"%(project)s\n" +"Zgłoszenie #%(ref)s %(subject)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Usunął zgłoszenie #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint updated

\n" +"

Hello %(user)s,
%(changer)s has updated an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See sprint\n" +" " +msgstr "" +"\n" +"

Sprint zaktualizowany

\n" +"

Witaj, uzytkownik %(user)s,
%(changer)s zaktualizował sprint w " +"projekcie %(project)s

\n" +"

Sprint %(name)s

\n" +" Zobacz sprint\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Sprint updated\n" +"Hello %(user)s, %(changer)s has updated a sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +msgstr "" +"\n" +"Sprint zaktualizowany\n" +"Witaj, użytkownik %(user)s, %(changer)s zaktualizował sprint w projekcie " +"%(project)s\n" +"Zobacz sprint %(name)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Zaktualizował sprint\"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New sprint created

\n" +"

Hello %(user)s,
%(changer)s has created a new sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See " +"sprint\n" +"

The Taiga Team

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

Utworzono nowy sprint

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s utworzył nowy sprint w " +"projekcie %(project)s

\n" +"

Sprint %(name)s

\n" +" Zobacz sprint\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New sprint created\n" +"Hello %(user)s, %(changer)s has created a new sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Utworzono nowy sprint\n" +"Witaj, użytkownik %(user)s, %(changer)s utworzył nowy sprint w projekcie " +"%(project)s\n" +"Zobacz sprint %(name)s at %(url)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Utworzył sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +"

The Taiga Team

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

Sprint usunięty

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s usunął sprint w " +"projekcie %(project)s

\n" +"

Sprint %(name)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Sprint deleted\n" +"Hello %(user)s, %(changer)s has deleted an sprint on %(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Sprint usunięty\n" +"Witaj, użytkownik %(user)s, %(changer)s usunął sprint w projekcie " +"%(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Skasował sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task updated

\n" +"

Hello %(user)s,
%(changer)s has updated a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +" " +msgstr "" +"\n" +"

Zadanie zaktualizowane

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s zaktualizował zadanie w " +"projekcie %(project)s

\n" +"

Zadanie #%(ref)s %(subject)s

\n" +" Zobacz zadanie\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Task updated\n" +"Hello %(user)s, %(changer)s has updated a task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Zadanie zaktualizowane\n" +"Witaj, użytkownik %(user)s, %(changer)s zaktualizował zadanie w projekcie " +"%(project)s\n" +"Zobacz zadanie #%(ref)s %(subject)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Zaktualizował zadanie #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New task created

\n" +"

Hello %(user)s,
%(changer)s has created a new task on " +"%(project)s

\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +"

The Taiga Team

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

Utworzono nowe zadanie

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s utworzył nowe zadanie w " +"projekcie %(project)s

\n" +"

Zadanie #%(ref)s %(subject)s

\n" +" Zobacz zadanie\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New task created\n" +"Hello %(user)s, %(changer)s has created a new task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Utworzono nowe zadanie\n" +"Witaj, użytkownik %(user)s, %(changer)s utworzył nowe zadanie w projekcie " +"%(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Utworzył zadanie #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Zadanie usunięte

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s usunął zadanie w " +"projekcie %(project)s

\n" +"

Zadanie #%(ref)s %(subject)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Task deleted\n" +"Hello %(user)s, %(changer)s has deleted a task on %(project)s\n" +"Task #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Zadanie usunięte\n" +"Witaj, użytkownik %(user)s, %(changer)s usunął zadanie w projekcie " +"%(project)s\n" +"Zadanie #%(ref)s %(subject)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Usunął zadanie #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story updated

\n" +"

Hello %(user)s,
%(changer)s has updated a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +" " +msgstr "" +"\n" +"

Historyjka użytkownika zaktualizowana

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s zaktualizował historyjkę " +"użytkownika w projekcie %(project)s

\n" +"

Historyjka użytkownika #%(ref)s %(subject)s

\n" +" Zobacz historyjkę użytkownika\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"User story updated\n" +"Hello %(user)s, %(changer)s has updated a user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Histroyjka użytkownika zaktualizowana\n" +"Witaj, użytkownik %(user)s, %(changer)s zaktualizował historyjkę użytkownika " +"w projekcie%(project)s\n" +"Zobacz historyjkę użytkownika #%(ref)s %(subject)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Zaktualizował HU #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New user story created

\n" +"

Hello %(user)s,
%(changer)s has created a new user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +"

The Taiga Team

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

Utworzono nową historyjkę użytkownika

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s utworzył nową historyjkę " +"użytkownika w projekcie %(project)s

\n" +"

Historyjka użytkownika #%(ref)s %(subject)s

\n" +" Zobacz historyjkę użytkownika\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New user story created\n" +"Hello %(user)s, %(changer)s has created a new user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Utworzono nową historyjkę użytkownika\n" +"Witaj, użytkownik %(user)s, %(changer)s utworzył nową historyjkę użytkownika " +"w projekcie %(project)s\n" +"Zobacz historyjkę użytkownika #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Utworzył HU #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Historyjka użytkownika usunięta

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s usunął historyjkę " +"użytkownika w projekcie %(project)s

\n" +"

Historyjka użytkonika #%(ref)s %(subject)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"User Story deleted\n" +"Hello %(user)s, %(changer)s has deleted a user story on %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Historyjka użytkownika usunięta\n" +"Witaj, użytkownik%(user)s, %(changer)s usunął historyjkę użytkownika w " +"projekcie %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Usunął HU #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki Page updated

\n" +"

Hello %(user)s,
%(changer)s has updated a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See Wiki Page\n" +" " +msgstr "" +"\n" +"

Strona Wiki zaktualizowana

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s zaktualizował stronę " +"Wiki w projekcie %(project)s

\n" +"

Strona Wiki %(page)s

\n" +" Zobacz stronę Wiki\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Wiki Page updated\n" +"\n" +"Hello %(user)s, %(changer)s has updated a wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +msgstr "" +"\n" +"Strona Wiki zaktualizowana\n" +"\n" +"Witaj, użytkownik %(user)s, %(changer)s zaktualizował stronę Wiki w " +"projekcie %(project)s\n" +"\n" +"Zobacz stronę Wiki %(page)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Zaktualizował stronę Wiki\"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New wiki page created

\n" +"

Hello %(user)s,
%(changer)s has created a new wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See " +"wiki page\n" +"

The Taiga Team

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

Utworzono nową stronę Wiki

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s utworzył nową stronę " +"Wiki w projekcie %(project)s

\n" +"

Strona Wiki %(page)s

\n" +" Zobacz stronę Wiki\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New wiki page created\n" +"\n" +"Hello %(user)s, %(changer)s has created a new wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Utworzono nową stronę Wiki\n" +"\n" +"Witaj, użytkownik %(user)s, %(changer)s utworzył nową stronę Wiki " +"%(project)s\n" +"\n" +"Zobacz stronę Wiki %(page)s at %(url)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Zaktualizował stronę Wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki page deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +"

The Taiga Team

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

Strona Wiki usunięta

\n" +"

Witaj, użytkownik %(user)s,
%(changer)s usunął stronę Wiki w " +"projekcie %(project)s

\n" +"

Strona Wiki %(page)s

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Wiki page deleted\n" +"\n" +"Hello %(user)s, %(changer)s has deleted a wiki page on %(project)s\n" +"\n" +"Wiki page %(page)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Strona Wiki usunięta\n" +"\n" +"Witaj, użytkownik %(user)s, %(changer)s usunął stronę Wiki w projekcie " +"%(project)s\n" +"\n" +"Strona Wiki %(page)s\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Usunął stronę Wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/validators.py:46 +msgid "Watchers contains invalid users" +msgstr "Obserwatorzy zawierają niepoprawnych użytkowników" + +#: taiga/projects/occ/mixins.py:35 +msgid "The version must be an integer" +msgstr "Wersja musi być integerem ;)" + +#: taiga/projects/occ/mixins.py:58 +msgid "The version parameter is not valid" +msgstr "Parametr wersji jest nieprawidłowy" + +#: taiga/projects/occ/mixins.py:74 +msgid "The version doesn't match with the current one" +msgstr "Podana wersja nie zgadza się z aktualną." + +#: taiga/projects/occ/mixins.py:93 +msgid "version" +msgstr "wersja" + +#: taiga/projects/permissions.py:39 +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" + +#: taiga/projects/serializers.py:240 +msgid "Email address is already taken" +msgstr "Tena adres e-mail jest już w użyciu" + +#: taiga/projects/serializers.py:252 +msgid "Invalid role for the project" +msgstr "Nieprawidłowa rola w projekcie" + +#: taiga/projects/serializers.py:397 +msgid "Default options" +msgstr "Domyślne opcje" + +#: taiga/projects/serializers.py:398 +msgid "User story's statuses" +msgstr "Statusy historyjek użytkownika" + +#: taiga/projects/serializers.py:399 +msgid "Points" +msgstr "Punkty" + +#: taiga/projects/serializers.py:400 +msgid "Task's statuses" +msgstr "Statusy zadań" + +#: taiga/projects/serializers.py:401 +msgid "Issue's statuses" +msgstr "Statusy zgłoszeń" + +#: taiga/projects/serializers.py:402 +msgid "Issue's types" +msgstr "Typu zgłoszeń" + +#: taiga/projects/serializers.py:403 +msgid "Priorities" +msgstr "Priorytety" + +#: taiga/projects/serializers.py:404 +msgid "Severities" +msgstr "Ważność" + +#: taiga/projects/serializers.py:405 +msgid "Roles" +msgstr "Role" + +#: taiga/projects/services/stats.py:85 +msgid "Future sprint" +msgstr "Przyszły sprint" + +#: taiga/projects/services/stats.py:102 +msgid "Project End" +msgstr "Zakończenie projektu" + +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +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:107 +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:110 +msgid "You don't have permissions to set this status to this task." +msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania" + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "kolejność HU" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "Kolejność tablicy zadań" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "Iokaina" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "Nie ma zadania z takim ID" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "ktoś" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:11 +#, python-format +msgid "" +"\n" +"

You have been invited to Taiga!

\n" +"

Hi! %(full_name)s has sent you an invitation to join project " +"%(project)s in Taiga.
Taiga is a Free, open Source Agile Project " +"Management Tool.

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

Zostałeś zaproszony do Taiga

\n" +"

Cześć! użytkownik %(full_name)s wysłał Ci zaproszenie do projektu " +"%(project)s w narzędziu Taiga.

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

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

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

A poniżej kilka słów od kogoś kto był tak miły
i zechciał " +"zaprosić Cię do projektu :)

\n" +"

%(extra)s

\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation to Taiga" +msgstr "Zaakceptuj zaproszenie do Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation" +msgstr "Zaakceptuj zaproszenie" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "The Taiga Team" +msgstr "Zespół Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:6 +#, python-format +msgid "" +"\n" +"You, or someone you know, has invited you to Taiga\n" +"\n" +"Hi! %(full_name)s has sent you an invitation to join a project called " +"%(project)s which is being managed on Taiga, a Free, open Source Agile " +"Project Management Tool.\n" +msgstr "" +"\n" +"Ty, lub ktoś kogo znasz wysłał zaproszenie do Taiga\n" +"\n" +"Cześć! Użytkownik %(full_name)s wysłał Ci zaproszenie do projektu " +"%(project)s utworzonego w narzędziu Taiga.\n" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:12 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" +"\n" +"A poniżej kilka słów od kogoś kto był tak miły
i zechciał zaprosić Cię " +"do projektu :)\n" +"\n" +"%(extra)s\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:18 +msgid "Accept your invitation to Taiga following this link:" +msgstr "Zaakceptuj zaproszenie do Taiga klikając w ten link: " + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:20 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Zaproszenie do dołączenia, do projektu '%(project)s'\n" + +#: taiga/projects/templates/emails/membership_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

You have been added to a project

\n" +"

Hello %(full_name)s,
you have been added to the project " +"%(project)s

\n" +" Go to " +"project\n" +"

The Taiga Team

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

Zostałeś dodany do projektu

\n" +"

Cześć %(full_name)s,
zostałeś dodany do projektu %(project)s\n" +" Idź do " +"projektu\n" +"

Zespół Taiga

\n" +" " + +#: taiga/projects/templates/emails/membership_notification-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"You have been added to a project\n" +"Hello %(full_name)s,you have been added to the project %(project)s\n" +"\n" +"See project at %(url)s\n" +msgstr "" +"\n" +"Zostałeś dodany do projektu\n" +"Cześć %(full_name)s, zostałęś dodany do projektu %(project)s\n" +"\n" +"Zobacz projektu tutaj: %(url)s\n" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Dodany do projektu '%(project)s'\n" + +#. Translators: Name of scrum project template. +#: taiga/projects/translations.py:28 +msgid "Scrum" +msgstr "Scrum" + +#. Translators: Description of scrum project template. +#: taiga/projects/translations.py:30 +msgid "" +"The agile product backlog in Scrum is a prioritized features list, " +"containing short descriptions of all functionality desired in the product. " +"When applying Scrum, it's not necessary to start a project with a lengthy, " +"upfront effort to document all requirements. The Scrum product backlog is " +"then allowed to grow and change as more is learned about the product and its " +"customers" +msgstr "" +"Scrum w oparciu o Taiga, które jest kompletnym narzędziem zawierającym " +"product backlog, opisy wszystkich funkcji i wskazówki to przyjemność. " +"Pracując w Scrumie nie musisz rozpoczynać projektu projektu od " +"dokumentowania wszystkiego a więc możesz zacząć pracować znacznie szybciej. " +"Product backlog jest na tyle elastyczny, że umożliwia szybki wzrost i oswaja " +"zmiany wraz z tym jak cały zespół poznaje specyfikę projektu i wymagania " +"klienta." + +#. Translators: Name of kanban project template. +#: taiga/projects/translations.py:33 +msgid "Kanban" +msgstr "Kanban" + +#. Translators: Description of kanban project template. +#: taiga/projects/translations.py:35 +msgid "" +"Kanban is a method for managing knowledge work with an emphasis on just-in-" +"time delivery while not overloading the team members. In this approach, the " +"process, from definition of a task to its delivery to the customer, is " +"displayed for participants to see and team members pull work from a queue." +msgstr "" +"Kanban jest metodą kierowania pracami, ze szczególnym naciskiem na dostawy " +"just-in-time, bez przeciążania członków zespołu. W tym podejściu, zadania są " +"wyświetlane dla klienta a członkowie zespołu wyciągają je z kolejki." + +#. Translators: User story point value (value = undefined) +#: taiga/projects/translations.py:43 +msgid "?" +msgstr "?" + +#. Translators: User story point value (value = 0) +#: taiga/projects/translations.py:45 +msgid "0" +msgstr "0" + +#. Translators: User story point value (value = 0.5) +#: taiga/projects/translations.py:47 +msgid "1/2" +msgstr "1/2" + +#. Translators: User story point value (value = 1) +#: taiga/projects/translations.py:49 +msgid "1" +msgstr "1" + +#. Translators: User story point value (value = 2) +#: taiga/projects/translations.py:51 +msgid "2" +msgstr "2" + +#. Translators: User story point value (value = 3) +#: taiga/projects/translations.py:53 +msgid "3" +msgstr "3" + +#. Translators: User story point value (value = 5) +#: taiga/projects/translations.py:55 +msgid "5" +msgstr "5" + +#. Translators: User story point value (value = 8) +#: taiga/projects/translations.py:57 +msgid "8" +msgstr "8" + +#. Translators: User story point value (value = 10) +#: taiga/projects/translations.py:59 +msgid "10" +msgstr "10" + +#. Translators: User story point value (value = 13) +#: taiga/projects/translations.py:61 +msgid "13" +msgstr "13" + +#. Translators: User story point value (value = 20) +#: taiga/projects/translations.py:63 +msgid "20" +msgstr "20" + +#. Translators: User story point value (value = 40) +#: taiga/projects/translations.py:65 +msgid "40" +msgstr "40" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:73 taiga/projects/translations.py:96 +#: taiga/projects/translations.py:112 +msgid "New" +msgstr "Nowe" + +#. Translators: User story status +#: taiga/projects/translations.py:76 +msgid "Ready" +msgstr "Gotowe" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:79 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 +msgid "In progress" +msgstr "W toku" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:82 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 +msgid "Ready for test" +msgstr "Gotowe do testów" + +#. Translators: User story status +#: taiga/projects/translations.py:85 +msgid "Done" +msgstr "Gotowe!" + +#. Translators: User story status +#: taiga/projects/translations.py:88 +msgid "Archived" +msgstr "Zarchiwizowane" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:102 taiga/projects/translations.py:118 +msgid "Closed" +msgstr "Zamknięte" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 +msgid "Needs Info" +msgstr "Potrzebne informacje" + +#. Translators: Issue status +#: taiga/projects/translations.py:122 +msgid "Postponed" +msgstr "Odroczone" + +#. Translators: Issue status +#: taiga/projects/translations.py:124 +msgid "Rejected" +msgstr "Odrzucone" + +#. Translators: Issue type +#: taiga/projects/translations.py:132 +msgid "Bug" +msgstr "Błąd" + +#. Translators: Issue type +#: taiga/projects/translations.py:134 +msgid "Question" +msgstr "Pytanie" + +#. Translators: Issue type +#: taiga/projects/translations.py:136 +msgid "Enhancement" +msgstr "Ulepszenie" + +#. Translators: Issue priority +#: taiga/projects/translations.py:144 +msgid "Low" +msgstr "Niski" + +#. Translators: Issue priority +#. Translators: Issue severity +#: taiga/projects/translations.py:146 taiga/projects/translations.py:159 +msgid "Normal" +msgstr "Normalny" + +#. Translators: Issue priority +#: taiga/projects/translations.py:148 +msgid "High" +msgstr "Wysoki" + +#. Translators: Issue severity +#: taiga/projects/translations.py:155 +msgid "Wishlist" +msgstr "Życzenie" + +#. Translators: Issue severity +#: taiga/projects/translations.py:157 +msgid "Minor" +msgstr "Pomniejsze" + +#. Translators: Issue severity +#: taiga/projects/translations.py:161 +msgid "Important" +msgstr "Istotne" + +#. Translators: Issue severity +#: taiga/projects/translations.py:163 +msgid "Critical" +msgstr "Krytyczne" + +#. Translators: User role +#: taiga/projects/translations.py:170 +msgid "UX" +msgstr "UX" + +#. Translators: User role +#: taiga/projects/translations.py:172 +msgid "Design" +msgstr "Design" + +#. Translators: User role +#: taiga/projects/translations.py:174 +msgid "Front" +msgstr "Front" + +#. Translators: User role +#: taiga/projects/translations.py:176 +msgid "Back" +msgstr "Back" + +#. Translators: User role +#: taiga/projects/translations.py:178 +msgid "Product Owner" +msgstr "Właściciel produktu" + +#. Translators: User role +#: taiga/projects/translations.py:180 +msgid "Stakeholder" +msgstr "Interesariusz" + +#: taiga/projects/userstories/api.py:156 +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:160 +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:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" +msgstr "" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "rola" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "Kolejność backlogu" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "kolejność sprintu" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "data zakończenia" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "wymaganie klienta" + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "wymaganie zespołu" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "wygenerowane ze zgłoszenia" + +#: taiga/projects/userstories/validators.py:28 +msgid "There's no user story with that id" +msgstr "Nie ma historyjki użytkownika z takim ID" + +#: taiga/projects/validators.py:28 +msgid "There's no project with that id" +msgstr "Nie ma projektu z takim ID" + +#: taiga/projects/validators.py:37 +msgid "There's no user story status with that id" +msgstr "Nie ma statusu historyjki użytkownika z takim ID" + +#: taiga/projects/validators.py:46 +msgid "There's no task status with that id" +msgstr "Nie ma statusu zadania z takim ID" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:56 +msgid "Votes" +msgstr "Głosy" + +#: taiga/projects/votes/models.py:55 +msgid "Vote" +msgstr "Głos" + +#: taiga/projects/wiki/api.py:66 +msgid "'content' parameter is mandatory" +msgstr "Parametr 'zawartość' jest wymagany" + +#: taiga/projects/wiki/api.py:69 +msgid "'project_id' parameter is mandatory" +msgstr "Parametr 'id_projektu' jest wymagany" + +#: taiga/projects/wiki/models.py:37 +msgid "last modifier" +msgstr "ostatnio zmodyfikowane przez" + +#: taiga/projects/wiki/models.py:70 +msgid "href" +msgstr "href" + +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "Dla pełengo diffa sprawdź API historii" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "Informacje osobiste" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "Uprawnienia" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "Ważne daty" + +#: taiga/users/api.py:111 +msgid "Duplicated email" +msgstr "Zduplikowany adres e-mail" + +#: taiga/users/api.py:113 +msgid "Not valid email" +msgstr "Niepoprawny adres e-mail" + +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Nieprawidłowa nazwa użytkownika lub adrs e-mail" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "E-mail wysłany poprawnie!" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Nieprawidłowy token." + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Należy podać bieżące hasło" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Należy podać nowe hasło" + +#: taiga/users/api.py:205 +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:208 +msgid "Invalid current password" +msgstr "Podałeś nieprawidłowe bieżące hasło" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Pola niekompletne" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Niepoprawny format obrazka" + +#: taiga/users/api.py:256 taiga/users/api.py:262 +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:289 taiga/users/api.py:297 taiga/users/api.py:300 +msgid "Invalid, are you sure the token is correct?" +msgstr "Niepoprawne, jesteś pewien, że token jest poprawny?" + +#: taiga/users/models.py:71 +msgid "superuser status" +msgstr "status SUPERUSER" + +#: taiga/users/models.py:72 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" +"Oznacza, że ten użytkownik posiada wszystkie uprawnienia bez konieczności " +"ich przydzielania." + +#: taiga/users/models.py:102 +msgid "username" +msgstr "nazwa użytkownika" + +#: taiga/users/models.py:103 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" +msgstr "Wymagane. 30 znaków. Liter, cyfr i znaków /./-/_" + +#: taiga/users/models.py:106 +msgid "Enter a valid username." +msgstr "Wprowadź poprawną nazwę użytkownika" + +#: taiga/users/models.py:109 +msgid "active" +msgstr "aktywny" + +#: taiga/users/models.py:110 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" +"Oznacza, że ten użytkownik ma być traktowany jako aktywny. Możesz to " +"odznaczyć zamiast usuwać konto." + +#: taiga/users/models.py:116 +msgid "biography" +msgstr "biografia" + +#: taiga/users/models.py:119 +msgid "photo" +msgstr "zdjęcie" + +#: taiga/users/models.py:120 +msgid "date joined" +msgstr "data dołączenia" + +#: taiga/users/models.py:122 +msgid "default language" +msgstr "domyślny język Taiga" + +#: taiga/users/models.py:124 +msgid "default theme" +msgstr "domyślny szablon Taiga" + +#: taiga/users/models.py:126 +msgid "default timezone" +msgstr "domyśla strefa czasowa" + +#: taiga/users/models.py:128 +msgid "colorize tags" +msgstr "kolory tagów" + +#: taiga/users/models.py:133 +msgid "email token" +msgstr "tokem e-mail" + +#: taiga/users/models.py:135 +msgid "new email address" +msgstr "nowy adres e-mail" + +#: taiga/users/models.py:203 +msgid "permissions" +msgstr "uprawnienia" + +#: taiga/users/serializers.py:62 +msgid "invalid" +msgstr "Niepoprawne" + +#: taiga/users/serializers.py:73 +msgid "Invalid username. Try with a different one." +msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną." + +#: taiga/users/services.py:53 taiga/users/services.py:57 +msgid "Username or password does not matches user." +msgstr "Nazwa użytkownika lub hasło są nieprawidłowe" + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Zmiana adresu e-mail

\n" +"

Witaj %(full_name)s,
potwierdź swój adres e-mail

\n" +" Potwierdź e-mail\n" +"

You can ignore this message if you did not request.

\n" +"

Zespół Taiga

\n" +" " + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Witaj %(full_name)s, potwierdź swój e-mail\n" +"\n" +"%(url)s\n" +"\n" +"Możesz zignorować tę wiadomość jeśli nie prosiłeś o jej wysłanie.\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "[Taiga] Zmienił e-mail" + +#: taiga/users/templates/emails/password_recovery-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Recover your password

\n" +"

Hello %(full_name)s,
you asked to recover your password

\n" +" Recover your password\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Odzyskiwanie hasła

\n" +"

Witaj %(full_name)s,
poprosiłeś o możliwość odzyskania hasła.\n" +" Odzyskaj " +"hasło\n" +"

Zignoruj tę wiadomość jeśli o nią nie prosiłeś.

\n" +"

Zespół taiga

\n" +" " + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Witaj %(full_name)s, poprosiłeś o możliwość odzyskania hasła.\n" +"\n" +"%(url)s\n" +"\n" +"Możesz zignorować tę wiadomość jeśli nie chcesz odzyskać hasła.\n" +"\n" +"---\n" +"Zespół Taiga\n" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "[Taiga] Odzyskał hasło" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" \n" +" " +msgstr "" +"\n" +" \n" +"

Dziękujemy za rejestrację w Taiga

\n" +"

amy nadzieję, że Ci się spodoba

\n" +"

WZbudowaliśmy narzędzie Taiga z potrzeby posiadania " +"rozwiązania na którym, będziemy mogli pracować bez ograniczeń funkcjonalnych " +"i czerpiąc przyjemność z doświadczeń wizualnych. Taiga stale przypomina nam " +"dlaczego uwielbiamy współpracować, kodować i tworzyć fantastyczny design.\n" +"

Taiga stworzyliśmy tak, by było pięknie, elegancko, prosto w " +"użyciu i zabawnie - nie zaniedbując jednocześnie elastyczności i możliwości " +"funkcjonalnych.

\n" +" Zespół Taiga\n" +" \n" +" " + +#: taiga/users/templates/emails/registered_user-body-html.jinja:23 +#, python-format +msgid "" +"\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" +"\n" +" Możesz usunąć swoje konto klikając tutaj\n" +" " + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" +"\n" +"Dziękujemy za rejestrację w Taiga\n" +"\n" +"Mamy nadzieję, że Ci się spodoba\n" +"\n" +"Zbudowaliśmy narzędzie Taiga z potrzeby posiadania rozwiązania na którym, " +"będziemy mogli pracować bez ograniczeń funkcjonalnych i czerpiąc przyjemność " +"z doświadczeń wizualnych. Taiga stale przypomina nam dlaczego uwielbiamy " +"współpracować, kodować i tworzyć fantastyczny design.\n" +"\n" +"Taiga stworzyliśmy tak, by było pięknie, elegancko, prosto w użyciu i " +"zabawnie - nie zaniedbując jednocześnie elastyczności i możliwości " +"funkcjonalnych.\n" +"\n" +"--\n" +"Zespół Taiga\n" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" +"\n" +"Możesz usunąć swoje konto z tego serwisu: %(url)s\n" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "Zostałeś zaTaigowany" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "Nie istnieje rola z takim ID" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "Duplikowanie wartości klucza. Klucz '{}' już istnieje." + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "klucz" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "URL" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "sekretny klucz" + +#: taiga/webhooks/models.py:39 +msgid "status code" +msgstr "kod statusu" + +#: taiga/webhooks/models.py:40 +msgid "request data" +msgstr "data żądania" + +#: taiga/webhooks/models.py:41 +msgid "request headers" +msgstr "nagłówki żądań" + +#: taiga/webhooks/models.py:42 +msgid "response data" +msgstr "dane odpowiedzi" + +#: taiga/webhooks/models.py:43 +msgid "response headers" +msgstr "nagłówki odpowiedzi" + +#: taiga/webhooks/models.py:44 +msgid "duration" +msgstr "czas trwania" diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 00000000..244f5689 --- /dev/null +++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,3596 @@ +# taiga-back.taiga. +# Copyright (C) 2014-2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +# Cléber Zavadniak , 2015 +# Thiago , 2015 +# Daniel Dias , 2015 +# David Barragán , 2015 +# Hevertton Barbosa , 2015 +# Kemel Zaidan , 2015 +# Marlon Carvalho , 2015 +# pedromvm , 2015 +# Renato Prado , 2015 +# Thiago , 2015 +# Walker de Alencar , 2015 +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/" +"taiga-back/language/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: taiga/auth/api.py:99 +msgid "Public register is disabled." +msgstr "Registro público está desabilitado. " + +#: taiga/auth/api.py:132 +msgid "invalid register type" +msgstr "tipo de registro inválido" + +#: taiga/auth/api.py:145 +msgid "invalid login type" +msgstr "tipo de login inválido" + +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 +msgid "invalid username" +msgstr "nome de usuário inválido" + +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 +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:73 +msgid "Username is already in use." +msgstr "Nome de usuário já está em uso." + +#: taiga/auth/services.py:76 +msgid "Email is already in use." +msgstr "Este e-mail já está em uso." + +#: taiga/auth/services.py:92 +msgid "Token not matches any valid invitation." +msgstr "Esse token não bate com nenhum convite." + +#: taiga/auth/services.py:120 +msgid "User is already registered." +msgstr "Este usuário já está registrado." + +#: taiga/auth/services.py:144 +msgid "Membership with user is already exists." +msgstr "Esse usuário já é membro." + +#: taiga/auth/services.py:170 +msgid "Error on creating new user." +msgstr "Erro ao criar um novo usuário." + +#: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 +msgid "Invalid token" +msgstr "Token inválido" + +#: taiga/base/api/fields.py:268 +msgid "This field is required." +msgstr "Este campo é obrigatório." + +#: taiga/base/api/fields.py:269 taiga/base/api/relations.py:311 +msgid "Invalid value." +msgstr "Valor inválido." + +#: taiga/base/api/fields.py:453 +#, python-format +msgid "'%s' value must be either True or False." +msgstr "O valor de '%s' deve ser ou True ou False." + +#: taiga/base/api/fields.py:517 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "" +"Entre uma 'slug' válida, consistindo de letras, números, underscores ou " +"hífens." + +#: taiga/base/api/fields.py:532 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "Escolha uma alternativa válida. %(value)s não está disponível." + +#: taiga/base/api/fields.py:595 +msgid "Enter a valid email address." +msgstr "Preencha com um e-mail válido." + +#: taiga/base/api/fields.py:637 +#, python-format +msgid "Date has wrong format. Use one of these formats instead: %s" +msgstr "A data está no formato errado. Use um desses no lugar: %s" + +#: taiga/base/api/fields.py:701 +#, python-format +msgid "Datetime has wrong format. Use one of these formats instead: %s" +msgstr "Formato da data e hora errado. Use um destes: %s" + +#: taiga/base/api/fields.py:771 +#, python-format +msgid "Time has wrong format. Use one of these formats instead: %s" +msgstr "Hora com formato errado. Use um destes: %s" + +#: taiga/base/api/fields.py:828 +msgid "Enter a whole number." +msgstr "Insira um número inteiro." + +#: taiga/base/api/fields.py:829 taiga/base/api/fields.py:882 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "Garanta que o valor é menor ou igual a %(limit_value)s." + +#: taiga/base/api/fields.py:830 taiga/base/api/fields.py:883 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "Garanta que o valor é maior ou igual a %(limit_value)s." + +#: taiga/base/api/fields.py:860 +#, python-format +msgid "\"%s\" value must be a float." +msgstr "O valor de \"%s\" deve ser decimal (float)." + +#: taiga/base/api/fields.py:881 +msgid "Enter a number." +msgstr "Insira um número." + +#: taiga/base/api/fields.py:884 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Garanta que não há mais que %s dígitos no total." + +#: taiga/base/api/fields.py:885 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Garanta que não há mais que %s casas decimais." + +#: taiga/base/api/fields.py:886 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Garanta que não há mais que %s dígitos antes do ponto decimal." + +#: taiga/base/api/fields.py:953 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "Nenhum arquivo enviado. Verifique o tipo de codificação no formulário." + +#: taiga/base/api/fields.py:954 +msgid "No file was submitted." +msgstr "Nenhum arquivo enviado." + +#: taiga/base/api/fields.py:955 +msgid "The submitted file is empty." +msgstr "O arquivo enviado está vazio." + +#: taiga/base/api/fields.py:956 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "" +"Garanta que o nome do arquivo tem no máximo %(max)d caracteres (no momento " +"tem %(length)d)." + +#: taiga/base/api/fields.py:957 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "Envie um arquivo ou marque o checkbox \"vazio\", não ambos." + +#: taiga/base/api/fields.py:997 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Envie uma imagem válida. O arquivo que você mandou ou não era uma imagem ou " +"está corrompido." + +#: taiga/base/api/pagination.py:115 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "Página não é \"última\", nem pode ser convertída para um inteiro." + +#: taiga/base/api/pagination.py:119 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Página inválida (%(page_number)s): %(message)s" + +#: taiga/base/api/permissions.py:61 +msgid "Invalid permission definition." +msgstr "Definição de permissão inválida." + +#: taiga/base/api/relations.py:221 +#, python-format +msgid "Invalid pk '%s' - object does not exist." +msgstr "Chave primária '%s' inválida - objeto não existe." + +#: taiga/base/api/relations.py:222 +#, python-format +msgid "Incorrect type. Expected pk value, received %s." +msgstr "Tipo incorreto. Esperado valor de chave primária, recebido %s." + +#: taiga/base/api/relations.py:310 +#, python-format +msgid "Object with %s=%s does not exist." +msgstr "Objeto com %s=%s não existe." + +#: taiga/base/api/relations.py:346 +msgid "Invalid hyperlink - No URL match" +msgstr "Hyperlink inválido - Nenhuma URL corresponde" + +#: taiga/base/api/relations.py:347 +msgid "Invalid hyperlink - Incorrect URL match" +msgstr "Hyperlink inválido - Corresponde a URL incorreta" + +#: taiga/base/api/relations.py:348 +msgid "Invalid hyperlink due to configuration error" +msgstr "Hyperlink inválido devido a erro de configuração" + +#: taiga/base/api/relations.py:349 +msgid "Invalid hyperlink - object does not exist." +msgstr "Hyperlink inválido - objeto não existe." + +#: taiga/base/api/relations.py:350 +#, python-format +msgid "Incorrect type. Expected url string, received %s." +msgstr "Tipo incorreto. Esperada string de url, recebido %s." + +#: taiga/base/api/serializers.py:296 +msgid "Invalid data" +msgstr "Dados inválidos" + +#: taiga/base/api/serializers.py:388 +msgid "No input provided" +msgstr "Nenhuma entrada providenciada" + +#: taiga/base/api/serializers.py:548 +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:559 +msgid "Expected a list of items." +msgstr "Esperada uma lista de itens." + +#: taiga/base/api/views.py:100 +msgid "Not found" +msgstr "Não encontrado" + +#: taiga/base/api/views.py:103 +msgid "Permission denied" +msgstr "Permissão negada" + +#: taiga/base/api/views.py:451 +msgid "Server application error" +msgstr "Erro no servidor da aplicação" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "Erro na conexão." + +#: taiga/base/exceptions.py:53 +msgid "Malformed request." +msgstr "Requisição mal-formada" + +#: taiga/base/exceptions.py:58 +msgid "Incorrect authentication credentials." +msgstr "Credenciais de autenticação incorretas." + +#: taiga/base/exceptions.py:63 +msgid "Authentication credentials were not provided." +msgstr "Credenciais de autenticação não informadas." + +#: taiga/base/exceptions.py:68 +msgid "You do not have permission to perform this action." +msgstr "Você não possui permissão para executar esta ação." + +#: taiga/base/exceptions.py:73 +#, python-format +msgid "Method '%s' not allowed." +msgstr "Método '%s' não é permitido" + +#: taiga/base/exceptions.py:81 +msgid "Could not satisfy the request's Accept header" +msgstr "Não foi possível satisfazer o cabeçalho Accept da requisição" + +#: taiga/base/exceptions.py:90 +#, python-format +msgid "Unsupported media type '%s' in request." +msgstr "Tipo de mídia '%s' não suportado na requisição." + +#: taiga/base/exceptions.py:98 +msgid "Request was throttled." +msgstr "Requisição foi sujeita a limites." + +#: taiga/base/exceptions.py:99 +#, python-format +msgid "Expected available in %d second%s." +msgstr "Esperado disponível em %d segundo%s." + +#: taiga/base/exceptions.py:113 +msgid "Unexpected error" +msgstr "Erro inesperado" + +#: taiga/base/exceptions.py:125 +msgid "Not found." +msgstr "Não encontrado." + +#: taiga/base/exceptions.py:130 +msgid "Method not supported for this endpoint." +msgstr "Método não suportado por esse endpoint." + +#: taiga/base/exceptions.py:138 taiga/base/exceptions.py:146 +msgid "Wrong arguments." +msgstr "Argumentos errados." + +#: taiga/base/exceptions.py:150 +msgid "Data validation error" +msgstr "Erro de validação dos dados" + +#: taiga/base/exceptions.py:162 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "Erro de Integridade para argumentos inválidos ou errados" + +#: taiga/base/exceptions.py:169 +msgid "Precondition error" +msgstr "Erro de pré-condição" + +#: taiga/base/filters.py:80 +msgid "Error in filter params types." +msgstr "Erro nos tipos de parâmetros do filtro." + +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 +msgid "'project' must be an integer value." +msgstr "'projeto' deve ser um valor inteiro." + +#: taiga/base/tags.py:25 +msgid "tags" +msgstr "tags" + +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "Taiga" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "Siga-nos no Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "Pegue o código no GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "Visite o nosso website" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "Taiga.io" + +#: taiga/base/templates/emails/base-body-html.jinja:423 +#: taiga/base/templates/emails/hero-body-html.jinja:397 +#: taiga/base/templates/emails/updates-body-html.jinja:459 +#, python-format +msgid "" +"\n" +" Taiga Support:\n" +" %(support_url)s\n" +"
\n" +" Contact us:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Mailing list:\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " +msgstr "" +"\n" +" Suporte Taiga:\n" +" %(support_url)s\n" +"
\n" +" Entre em contato:" +"\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Lista de e-mail:" +"\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Você foi Taigatizado" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

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

Você foi taigatizado!\n" +"

Bem vindo ao Taiga, " +"ferramenta de gerenciamento de projeto ágil e Open Source

\n" +" " + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "[Taiga] Atualizações" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "Atualizações" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

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

comentário:" +"

\n" +"

" +"%(comment)s

\n" +" " + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" +"\n" +" Comentário: %(comment)s\n" +" " + +#: taiga/export_import/api.py:103 +msgid "We needed at least one role" +msgstr "Nós precisamos de pelo menos uma função" + +#: taiga/export_import/api.py:197 +msgid "Needed dump file" +msgstr "Necessário de arquivo de restauração" + +#: taiga/export_import/api.py:204 +msgid "Invalid dump format" +msgstr "Formato de aquivo de restauração inválido" + +#: taiga/export_import/dump_service.py:96 +msgid "error importing project data" +msgstr "erro ao importar informações de projeto" + +#: taiga/export_import/dump_service.py:109 +msgid "error importing lists of project attributes" +msgstr "erro importando lista de atributos do projeto" + +#: taiga/export_import/dump_service.py:114 +msgid "error importing default project attributes values" +msgstr "erro importando valores de atributos do projeto padrão" + +#: taiga/export_import/dump_service.py:124 +msgid "error importing custom attributes" +msgstr "erro importando atributos personalizados" + +#: taiga/export_import/dump_service.py:129 +msgid "error importing roles" +msgstr "erro importando funcões" + +#: taiga/export_import/dump_service.py:144 +msgid "error importing memberships" +msgstr "erro importando filiações" + +#: taiga/export_import/dump_service.py:149 +msgid "error importing sprints" +msgstr "erro importando sprints" + +#: taiga/export_import/dump_service.py:154 +msgid "error importing wiki pages" +msgstr "erro importando páginas wiki" + +#: taiga/export_import/dump_service.py:159 +msgid "error importing wiki links" +msgstr "erro importando wiki links" + +#: taiga/export_import/dump_service.py:164 +msgid "error importing issues" +msgstr "erro importando casos" + +#: taiga/export_import/dump_service.py:169 +msgid "error importing user stories" +msgstr "erro importando user stories" + +#: taiga/export_import/dump_service.py:174 +msgid "error importing tasks" +msgstr "erro importando tarefas" + +#: taiga/export_import/dump_service.py:179 +msgid "error importing tags" +msgstr "erro importando tags" + +#: taiga/export_import/dump_service.py:183 +msgid "error importing timelines" +msgstr "erro importando linha do tempo" + +#: taiga/export_import/serializers.py:163 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" não encontrado nesse projeto" + +#: taiga/export_import/serializers.py:428 +#: taiga/projects/custom_attributes/serializers.py:103 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "conteúdo inválido. Deve ser {\"key\": \"value\",...}" + +#: taiga/export_import/serializers.py:443 +#: taiga/projects/custom_attributes/serializers.py:118 +msgid "It contain invalid custom fields." +msgstr "Contém campos personalizados inválidos" + +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 +msgid "Name duplicated for the project" +msgstr "Nome duplicado para o projeto" + +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 +msgid "Error generating project dump" +msgstr "Erro gerando arquivo de restauração do projeto" + +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 +msgid "Error loading project dump" +msgstr "Erro carregando arquivo de restauração do projeto" + +#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump generated

\n" +"

Hello %(user)s,

\n" +"

Your dump from project %(project)s has been correctly generated.\n" +"

You can download it here:

\n" +" Download the dump file\n" +"

This file will be deleted on %(deletion_date)s.

\n" +"

The Taiga Team

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

Restauração do projeto gerado

\n" +"

Bem vindo %(user)s,

\n" +"

Seu arquivo de restauração de projeto %(project)s foi corretamente " +"gerado.

\n" +"

Você pode baixa-lo aqui:

\n" +" Download do arquivo de restauração\n" +"

Esse arquivo será apagado em %(deletion_date)s.

\n" +"

O time Taiga

\n" +" " + +#: taiga/export_import/templates/emails/dump_project-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your dump from project %(project)s has been correctly generated. You can " +"download it here:\n" +"\n" +"%(url)s\n" +"\n" +"This file will be deleted on %(deletion_date)s.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Olá %(user)s,\n" +"\n" +"Seu arquivo de restauração do projeto %(project)s foi gerado corretamente. " +"Você pode baixar ele aqui:\n" +"\n" +"%(url)s\n" +"\n" +"Esse arquivo será apagado em %(deletion_date)s.\n" +"\n" +"---\n" +"O time Taiga\n" + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "[%(project)s] Seu arquivo de restauração do projeto foi criado" + +#: taiga/export_import/templates/emails/export_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project %(project)s has not been exported correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"

Olá %(user)s,

\n" +"

Seu projeto %(project)s não foi importado corretamente.

\n" +"

Os administradores de sistema Taiga foram informados.
Por favor, " +"tente novamente ou entre em contato com o time de suporte em\n" +" %(support_email)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/export_import/templates/emails/export_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"Your project %(project)s has not been exported correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Olá %(user)s,\n" +"\n" +"%(error_message)s\n" +"Seu projeto %(project)s não foi importado corretamente.\n" +"\n" +"O time de administradores de sistema Taiga foram informados.\n" +"\n" +"Por favor, tente novamente ou contate o time de suporte em " +"%(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "[%(project)s] %(error_subject)s" + +#: taiga/export_import/templates/emails/import_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project has not been importer correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"

Olá %(user)s,

\n" +"

Seu projeto foi importado corretamente.

\n" +"

O time de administradores de sistema do Taiga foram informados.
" +"Por favor, tentar novamente ou entrar em contado com o time de suporte em\n" +" %(support_email)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/export_import/templates/emails/import_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Your project has not been importer correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Olá %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Seu projeto não foi importado corretamente.\n" +"\n" +"O time de administradores de sistema do Taiga foram informados.
Por " +"favor, tentar novamente ou entrar em contado com o time de suporte em " +"%(support_email)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "[Taiga] %(error_subject)s" + +#: taiga/export_import/templates/emails/load_dump-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump imported

\n" +"

Hello %(user)s,

\n" +"

Your project dump has been correctly imported.

\n" +" Go to %(project)s\n" +"

The Taiga Team

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

Restauração do projeto importada

\n" +"

Olá %(user)s,

\n" +"

Seu arquivo de restauração foi importado corretamente.

\n" +" Ir para %(project)s\n" +"

O Time Taiga

\n" +" " + +#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your project dump has been correctly imported.\n" +"\n" +"You can see the project %(project)s here:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Olá %(user)s,\n" +"\n" +"Sua restauração foi corretamente importada.\n" +"\n" +"Você pode ver seu projeto %(project)s aqui:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "[%(project)s] A restauração do seu projeto foi importada" + +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "Autenticação necessária" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "Nome" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "Ícone da url" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "web" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "descrição" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "Próxima url" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "chave secreta para cifrar os tokens da aplicação" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "usuário" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "aplicação" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 +msgid "full name" +msgstr "nome completo" + +#: taiga/feedback/models.py:25 taiga/users/models.py:108 +msgid "email address" +msgstr "endereço de e-mail" + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "comentário" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 +msgid "created date" +msgstr "data de criação" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

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

Resposta

\n" +"

Taiga recebeu resposta de %(full_name)s <%(email)s>

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

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

Comentário

\n" +"

%(comment)s

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "Informação extra" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" +"---------\n" +"- De: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comentário:\n" +"%(comment)s\n" +"---------" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "- Informação extra:" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" +"\n" +"[Taiga] Resposta de %(full_name)s <%(email)s>\n" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "A carga não é um json válido" + +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 +msgid "The project doesn't exist" +msgstr "O projeto não existe" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "Assinatura Ruim" + +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 +msgid "The referenced element doesn't exist" +msgstr "O elemento referenciado não existe" + +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 +msgid "The status doesn't exist" +msgstr "O estatus não existe" + +#: taiga/hooks/bitbucket/event_hooks.py:97 +msgid "Status changed from BitBucket commit" +msgstr "Status alterado em Bitbucket commit" + +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Informação de caso inválida" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Caso criado por [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "Caso criado pelo Bitbucket." + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Informação de comentário de caso inválido" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Comentário por [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"Comentário pelo Bitbucket:\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 +#, python-brace-format +msgid "" +"Status changed by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +msgstr "" +"Status alterado por [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." + +#: taiga/hooks/github/event_hooks.py:107 +msgid "Status changed from GitHub commit." +msgstr "Status alterado por commit do Github." + +#: taiga/hooks/github/event_hooks.py:157 +#, python-brace-format +msgid "" +"Issue created by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Caso criado por [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/github/event_hooks.py:168 +msgid "Issue created from GitHub." +msgstr "Caso criado pelo Github." + +#: taiga/hooks/github/event_hooks.py:200 +#, python-brace-format +msgid "" +"Comment by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Comentário por [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:211 +#, python-brace-format +msgid "" +"Comment From GitHub:\n" +"\n" +"{message}" +msgstr "" +"Comentário pelo Github:\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:86 +msgid "Status changed from GitLab commit" +msgstr "Status alterado por um commit de Gitlab" + +#: taiga/hooks/gitlab/event_hooks.py:128 +msgid "Created from GitLab" +msgstr "Criado pelo Gitlab" + +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Comentário por [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"Comentário pelo GitLab:\n" +"\n" +"{message}" + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:51 +msgid "View project" +msgstr "Ver projeto" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:53 +msgid "View milestones" +msgstr "Ver marco de progresso" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "Ver user stories" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 +msgid "View tasks" +msgstr "Ver tarefa" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:68 +msgid "View issues" +msgstr "Ver casos" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 +msgid "View wiki pages" +msgstr "Ver página wiki" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 +msgid "View wiki links" +msgstr "Ver links wiki" + +#: taiga/permissions/permissions.py:38 +msgid "Request membership" +msgstr "Solicitar filiação" + +#: taiga/permissions/permissions.py:39 +msgid "Add user story to project" +msgstr "Adicionar user story para projeto" + +#: taiga/permissions/permissions.py:40 +msgid "Add comments to user stories" +msgstr "Adicionar comentários para user story" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to tasks" +msgstr "Adicionar comentário para tarefa" + +#: taiga/permissions/permissions.py:42 +msgid "Add issues" +msgstr "Adicionar casos" + +#: taiga/permissions/permissions.py:43 +msgid "Add comments to issues" +msgstr "Adicionar comentários aos casos" + +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 +msgid "Add wiki page" +msgstr "Adicionar página wiki" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 +msgid "Modify wiki page" +msgstr "modificar página wiki" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 +msgid "Add wiki link" +msgstr "Adicionar link wiki" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 +msgid "Modify wiki link" +msgstr "Modificar wiki link" + +#: taiga/permissions/permissions.py:54 +msgid "Add milestone" +msgstr "Adicionar marco de progresso" + +#: taiga/permissions/permissions.py:55 +msgid "Modify milestone" +msgstr "Modificar marco de progresso" + +#: taiga/permissions/permissions.py:56 +msgid "Delete milestone" +msgstr "Remover marco de progresso" + +#: taiga/permissions/permissions.py:58 +msgid "View user story" +msgstr "Ver user story" + +#: taiga/permissions/permissions.py:59 +msgid "Add user story" +msgstr "Adicionar user story" + +#: taiga/permissions/permissions.py:60 +msgid "Modify user story" +msgstr "Modificar user story" + +#: taiga/permissions/permissions.py:61 +msgid "Delete user story" +msgstr "Deletar user story" + +#: taiga/permissions/permissions.py:64 +msgid "Add task" +msgstr "Adicionar tarefa" + +#: taiga/permissions/permissions.py:65 +msgid "Modify task" +msgstr "Modificar tarefa" + +#: taiga/permissions/permissions.py:66 +msgid "Delete task" +msgstr "Deletar tarefa" + +#: taiga/permissions/permissions.py:69 +msgid "Add issue" +msgstr "Adicionar caso" + +#: taiga/permissions/permissions.py:70 +msgid "Modify issue" +msgstr "Modificar caso" + +#: taiga/permissions/permissions.py:71 +msgid "Delete issue" +msgstr "Deletar caso" + +#: taiga/permissions/permissions.py:76 +msgid "Delete wiki page" +msgstr "Deletar página wiki" + +#: taiga/permissions/permissions.py:81 +msgid "Delete wiki link" +msgstr "Deletar link wiki" + +#: taiga/permissions/permissions.py:85 +msgid "Modify project" +msgstr "Modificar projeto" + +#: taiga/permissions/permissions.py:86 +msgid "Add member" +msgstr "Adicionar membro" + +#: taiga/permissions/permissions.py:87 +msgid "Remove member" +msgstr "Remover membro" + +#: taiga/permissions/permissions.py:88 +msgid "Delete project" +msgstr "Deletar projeto" + +#: taiga/permissions/permissions.py:89 +msgid "Admin project values" +msgstr "Valores projeto admin" + +#: taiga/permissions/permissions.py:90 +msgid "Admin roles" +msgstr "Funções Admin" + +#: taiga/projects/api.py:202 +msgid "Not valid template name" +msgstr "Nome de template inválido" + +#: taiga/projects/api.py:205 +msgid "Not valid template description" +msgstr "Descrição de template inválida" + +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 +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:511 +msgid "You don't have permisions to see that." +msgstr "Você não tem permissão para ver isso" + +#: taiga/projects/attachments/api.py:47 +msgid "Partial updates are not supported" +msgstr "Atualizações parciais não são suportadas" + +#: taiga/projects/attachments/api.py:62 +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:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 +#: taiga/userstorage/models.py:25 +msgid "owner" +msgstr "dono" + +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 +msgid "project" +msgstr "projeto" + +#: taiga/projects/attachments/models.py:56 +msgid "content type" +msgstr "tipo de conteúdo" + +#: taiga/projects/attachments/models.py:58 +msgid "object id" +msgstr "identidade de objeto" + +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "data modificação" + +#: taiga/projects/attachments/models.py:69 +msgid "attached file" +msgstr "arquivo anexado" + +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "sha1" + +#: taiga/projects/attachments/models.py:73 +msgid "is deprecated" +msgstr "está obsoleto" + +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 +msgid "order" +msgstr "ordem" + +#: taiga/projects/choices.py:21 +msgid "AppearIn" +msgstr "Aparece em" + +#: taiga/projects/choices.py:22 +msgid "Jitsi" +msgstr "Jitsi" + +#: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "Personalizado" + +#: taiga/projects/choices.py:24 +msgid "Talky" +msgstr "Talky" + +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Texto" + +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Multi-linha" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "Data" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "Tipo" + +#: taiga/projects/custom_attributes/models.py:87 +msgid "values" +msgstr "valores" + +#: taiga/projects/custom_attributes/models.py:97 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 +msgid "user story" +msgstr "user story" + +#: taiga/projects/custom_attributes/models.py:112 +msgid "task" +msgstr "tarefa" + +#: taiga/projects/custom_attributes/models.py:127 +msgid "issue" +msgstr "caso" + +#: taiga/projects/custom_attributes/serializers.py:57 +msgid "Already exists one with the same name." +msgstr "Já existe um com o mesmo nome." + +#: taiga/projects/history/api.py:70 +msgid "Comment already deleted" +msgstr "Comentário já apagado" + +#: taiga/projects/history/api.py:89 +msgid "Comment not deleted" +msgstr "Comentário não apagado" + +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "Alterar" + +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "Criar" + +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "Apagar" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "%(role)s pontos de função" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:193 +msgid "from" +msgstr "de" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:199 +msgid "to" +msgstr "para" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "Adicionar novos anexos" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "Atualizar anexo" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "obsoleto" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "não-obsoleto" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "Anexo apagado" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "adicionado" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "removido" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 +msgid "Unassigned" +msgstr "Não-atribuído" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:211 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "-apagado-" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "para:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "de:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "Adicionado" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "Alterado" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "Apagado" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "acrescentado:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "removido:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "De:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "Para:" + +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 +msgid "content" +msgstr "conteúdo" + +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "nota bloqueada" + +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "sprint" + +#: taiga/projects/issues/api.py:160 +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:164 +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:168 +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:172 +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:176 +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." + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "ref" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "status" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "severidade" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "prioridade" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "marco de progresso" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "data de término" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "assunto" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "assinado a" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "referência externa" + +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "contagem" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "Curtidas" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "Curtir" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 +msgid "slug" +msgstr "slug" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "data de início estimada" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "data de encerramento estimada" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 +msgid "is closed" +msgstr "está fechado" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "disponibilidade" + +#: taiga/projects/milestones/models.py:75 +msgid "The estimated start must be previous to the estimated finish." +msgstr "A estimativa de inicio deve ser anterior a estimativa de encerramento" + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "Não há sprint com esse id" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "está bloqueado" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "'{param}' parameter is mandatory" +msgstr "'{param}' parametro é mandatório" + +#: taiga/projects/mixins/ordering.py:51 +msgid "'project' parameter is mandatory" +msgstr "'project' parametro é mandatório" + +#: taiga/projects/models.py:66 +msgid "email" +msgstr "email" + +#: taiga/projects/models.py:68 +msgid "create at" +msgstr "criado em" + +#: taiga/projects/models.py:70 taiga/users/models.py:130 +msgid "token" +msgstr "token" + +#: taiga/projects/models.py:76 +msgid "invitation extra text" +msgstr "texto extra de convite" + +#: taiga/projects/models.py:79 +msgid "user order" +msgstr "ordem de usuário" + +#: taiga/projects/models.py:89 +msgid "The user is already member of the project" +msgstr "O usuário já é membro do projeto" + +#: taiga/projects/models.py:104 +msgid "default points" +msgstr "pontos padrão" + +#: taiga/projects/models.py:108 +msgid "default US status" +msgstr "status de US padrão" + +#: taiga/projects/models.py:112 +msgid "default task status" +msgstr "status padrão de tarefa" + +#: taiga/projects/models.py:115 +msgid "default priority" +msgstr "prioridade padrão" + +#: taiga/projects/models.py:118 +msgid "default severity" +msgstr "severidade padrão" + +#: taiga/projects/models.py:122 +msgid "default issue status" +msgstr "status padrão de caso" + +#: taiga/projects/models.py:126 +msgid "default issue type" +msgstr "tipo padrão de caso" + +#: taiga/projects/models.py:147 +msgid "members" +msgstr "membros" + +#: taiga/projects/models.py:150 +msgid "total of milestones" +msgstr "total de marcos de progresso" + +#: taiga/projects/models.py:151 +msgid "total story points" +msgstr "pontos totais de US" + +#: taiga/projects/models.py:154 taiga/projects/models.py:614 +msgid "active backlog panel" +msgstr "painel de backlog ativo" + +#: taiga/projects/models.py:156 taiga/projects/models.py:616 +msgid "active kanban panel" +msgstr "painel de kanban ativo" + +#: taiga/projects/models.py:158 taiga/projects/models.py:618 +msgid "active wiki panel" +msgstr "painel de wiki ativo" + +#: taiga/projects/models.py:160 taiga/projects/models.py:620 +msgid "active issues panel" +msgstr "painel de casos ativo" + +#: taiga/projects/models.py:163 taiga/projects/models.py:623 +msgid "videoconference system" +msgstr "sistema de vídeo conferência" + +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "informação extra de vídeo conferência" + +#: taiga/projects/models.py:170 +msgid "creation template" +msgstr "template de criação" + +#: taiga/projects/models.py:173 +msgid "anonymous permissions" +msgstr "permissão anônima" + +#: taiga/projects/models.py:177 +msgid "user permissions" +msgstr "permissão de usuário" + +#: taiga/projects/models.py:180 +msgid "is private" +msgstr "é privado" + +#: taiga/projects/models.py:191 +msgid "tags colors" +msgstr "cores de tags" + +#: taiga/projects/models.py:383 +msgid "modules config" +msgstr "configurações de módulos" + +#: taiga/projects/models.py:402 +msgid "is archived" +msgstr "está arquivado" + +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 +msgid "color" +msgstr "cor" + +#: taiga/projects/models.py:406 +msgid "work in progress limit" +msgstr "trabalho no limite de progresso" + +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 +msgid "value" +msgstr "valor" + +#: taiga/projects/models.py:611 +msgid "default owner's role" +msgstr "função padrão para dono " + +#: taiga/projects/models.py:627 +msgid "default options" +msgstr "opções padrão" + +#: taiga/projects/models.py:628 +msgid "us statuses" +msgstr "status de US" + +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 +msgid "points" +msgstr "pontos" + +#: taiga/projects/models.py:630 +msgid "task statuses" +msgstr "status de tarefa" + +#: taiga/projects/models.py:631 +msgid "issue statuses" +msgstr "status de casos" + +#: taiga/projects/models.py:632 +msgid "issue types" +msgstr "tipos de caso" + +#: taiga/projects/models.py:633 +msgid "priorities" +msgstr "prioridades" + +#: taiga/projects/models.py:634 +msgid "severities" +msgstr "severidades" + +#: taiga/projects/models.py:635 +msgid "roles" +msgstr "funções" + +#: taiga/projects/notifications/choices.py:28 +msgid "Involved" +msgstr "" + +#: taiga/projects/notifications/choices.py:29 +msgid "All" +msgstr "" + +#: taiga/projects/notifications/choices.py:30 +msgid "None" +msgstr "" + +#: taiga/projects/notifications/models.py:61 +msgid "created date time" +msgstr "data de criação" + +#: taiga/projects/notifications/models.py:63 +msgid "updated date time" +msgstr "data de atualização" + +#: taiga/projects/notifications/models.py:65 +msgid "history entries" +msgstr "histórico de entradas" + +#: taiga/projects/notifications/models.py:68 +msgid "notify users" +msgstr "notificar usuário" + +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "Observado" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 +msgid "Notify exists for specified user and project" +msgstr "Existe notificação para usuário e projeto especifcado" + +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "Valor inválido para nível de notificação" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue updated

\n" +"

Hello %(user)s,
%(changer)s has updated an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +" " +msgstr "" +"\n" +"

Caso atualizado

\n" +"

Olá %(user)s,
%(changer)s atualizou caso em %(project)s

\n" +"

Caso #%(ref)s %(subject)s

\n" +" Ver caso\n" +"\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Issue updated\n" +"Hello %(user)s, %(changer)s has updated an issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Caso atualizado\n" +"Olá %(user)s, %(changer)s atualizou um caso em %(project)s\n" +"Ver caso #%(ref)s %(subject)s em %(url)s\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Atualizou o caso #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New issue created

\n" +"

Hello %(user)s,
%(changer)s has created a new issue on " +"%(project)s

\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +"

The Taiga Team

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

Novo caso criado

\n" +"

Olá %(user)s,
%(changer)s criou um novo caso em %(project)s

\n" +"

Caso #%(ref)s %(subject)s

\n" +" Ver caso\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New issue created\n" +"Hello %(user)s, %(changer)s has created a new issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Novo caso criado\n" +"Olá %(user)s, %(changer)s criou um novo caso em %(project)s\n" +"Ver caso #%(ref)s %(subject)s em %(url)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Criou o caso #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Caso apagado

\n" +"

Olá %(user)s,
%(changer)s apagou um caso em %(project)s

\n" +"

Caso #%(ref)s %(subject)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Issue deleted\n" +"Hello %(user)s, %(changer)s has deleted an issue on %(project)s\n" +"Issue #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Caso apagado\n" +"Olá %(user)s, %(changer)s apagou um caso em %(project)s\n" +"caso #%(ref)s %(subject)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Apagou o caso #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint updated

\n" +"

Hello %(user)s,
%(changer)s has updated an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See sprint\n" +" " +msgstr "" +"\n" +"

Sprint atualizado

\n" +"

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

Sprint %(name)s

\n" +" Ver sprint\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Sprint updated\n" +"Hello %(user)s, %(changer)s has updated a sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +msgstr "" +"\n" +"Sprint atualizado\n" +"Olá %(user)s, %(changer)s atualizou sprint em %(project)s\n" +"Ver sprint %(name)s em %(url)s\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Atualizou o sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New sprint created

\n" +"

Hello %(user)s,
%(changer)s has created a new sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See " +"sprint\n" +"

The Taiga Team

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

Novo sprint criado

\n" +"

Olá %(user)s,
%(changer)s criou novo sprint em %(project)s

\n" +"

Sprint %(name)s

\n" +" Ver " +"sprint\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New sprint created\n" +"Hello %(user)s, %(changer)s has created a new sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Novo sprint criado\n" +"Olá %(user)s, %(changer)s criou um novo sprint on %(project)s\n" +"Ver sprint %(name)s em %(url)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Criou o sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +"

The Taiga Team

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

Sprint apagado

\n" +"

Olá %(user)s,
%(changer)s apagou sprint em %(project)s

\n" +"

Sprint %(name)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Sprint deleted\n" +"Hello %(user)s, %(changer)s has deleted an sprint on %(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Sprint deletado\n" +"Olá %(user)s, %(changer)s apagado sprint em %(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Apagou o Sprint \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task updated

\n" +"

Hello %(user)s,
%(changer)s has updated a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +" " +msgstr "" +"\n" +"

Tarefa atualizada

\n" +"

Olá %(user)s,
%(changer)s atualizou tarefa em %(project)s

\n" +"

Tarefa #%(ref)s %(subject)s

\n" +" Ver tarefa\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Task updated\n" +"Hello %(user)s, %(changer)s has updated a task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Tarefa atualizada\n" +"Olá %(user)s, %(changer)s atualizou tarefa em %(project)s\n" +"Ver tarefa #%(ref)s %(subject)s em %(url)s\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Atualizou a tarefa #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New task created

\n" +"

Hello %(user)s,
%(changer)s has created a new task on " +"%(project)s

\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +"

The Taiga Team

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

Nova tarefa criada

\n" +"

Olá %(user)s,
%(changer)s criou nova tarefa em %(project)s

\n" +"

Task #%(ref)s %(subject)s

\n" +" Ver tarefa\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New task created\n" +"Hello %(user)s, %(changer)s has created a new task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Nova tarefa criada\n" +"Olá %(user)s, %(changer)s criou uma nova tarefa em %(project)s\n" +"Ver tarefa #%(ref)s %(subject)s em %(url)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Criou a tarefa #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Tarefa apagada

\n" +"

Olá %(user)s,
%(changer)s apagou uma tarefa em %(project)s

\n" +"

Tarefa #%(ref)s %(subject)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Task deleted\n" +"Hello %(user)s, %(changer)s has deleted a task on %(project)s\n" +"Task #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Tarefa apagada \n" +"Olá %(user)s, %(changer)s apagou tarefa em %(project)s\n" +"Tarefa #%(ref)s %(subject)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Apagou a tarefa #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story updated

\n" +"

Hello %(user)s,
%(changer)s has updated a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +" " +msgstr "" +"\n" +"

User Story atualizada

\n" +"

Olá %(user)s,
%(changer)s atualizou a user story em %(project)s\n" +"

User Story #%(ref)s %(subject)s

\n" +" Ver user story\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"User story updated\n" +"Hello %(user)s, %(changer)s has updated a user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"User story atualizada\n" +"Olá %(user)s, %(changer)s atualizou a user story em %(project)s\n" +"Ver user story #%(ref)s %(subject)s em %(url)s\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Atualizou a US #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New user story created

\n" +"

Hello %(user)s,
%(changer)s has created a new user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +"

The Taiga Team

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

Nova user story criada

\n" +"

Olá %(user)s,
%(changer)s criou nova user story em %(project)s\n" +"

User Story #%(ref)s %(subject)s

\n" +" Ver user story\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New user story created\n" +"Hello %(user)s, %(changer)s has created a new user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Nova user story criada\n" +"Olá %(user)s, %(changer)s criou nova user story em %(project)s\n" +"Ver user story #%(ref)s %(subject)s em %(url)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Criou a US #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

User Story apagada

\n" +"

Olá %(user)s,
%(changer)s apagou uma user story em %(project)s\n" +"

User Story #%(ref)s %(subject)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"User Story deleted\n" +"Hello %(user)s, %(changer)s has deleted a user story on %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"User Story apagada\n" +"Olá %(user)s, %(changer)s apagou user story em %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Apagou a US #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki Page updated

\n" +"

Hello %(user)s,
%(changer)s has updated a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See Wiki Page\n" +" " +msgstr "" +"\n" +"

Página Wiki atualizada

\n" +"

Olá %(user)s,
%(changer)s atualizou a página wiki em%(project)s\n" +"

Página Wiki %(page)s

\n" +" Ver página Wiki\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Wiki Page updated\n" +"\n" +"Hello %(user)s, %(changer)s has updated a wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +msgstr "" +"\n" +"Página Wiki atualizada\n" +"\n" +"Olá %(user)s, %(changer)s atualizou a página wiki em %(project)s\n" +"\n" +"Ver página wiki %(page)s em %(url)s\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Atualizou a página wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New wiki page created

\n" +"

Hello %(user)s,
%(changer)s has created a new wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See " +"wiki page\n" +"

The Taiga Team

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

Nova página wiki criada

\n" +"

Olá %(user)s,
%(changer)s criou uma nova página wiki em " +"%(project)s

\n" +"

Página wiki %(page)s

\n" +" Ver " +"página wiki\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New wiki page created\n" +"\n" +"Hello %(user)s, %(changer)s has created a new wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Nova página wiki criada\n" +"\n" +"Olá %(user)s, %(changer)s criou uma página wiki em %(project)s\n" +"\n" +"Ver página wiki %(page)s em %(url)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Criou a página wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki page deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +"

The Taiga Team

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

Página Wiki apagada

\n" +"

Olá %(user)s,
%(changer)s apagou uma página wiki em %(project)s\n" +"

Página Wiki %(page)s

\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Wiki page deleted\n" +"\n" +"Hello %(user)s, %(changer)s has deleted a wiki page on %(project)s\n" +"\n" +"Wiki page %(page)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Página wiki apagada\n" +"\n" +"Olá %(user)s, %(changer)s apagou uma página wiki em %(project)s\n" +"\n" +"Página Wiki %(page)s\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Apagou a página Wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/validators.py:46 +msgid "Watchers contains invalid users" +msgstr "Observadores contém usuários inválidos" + +#: taiga/projects/occ/mixins.py:35 +msgid "The version must be an integer" +msgstr "A versão precisa ser um inteiro" + +#: taiga/projects/occ/mixins.py:58 +msgid "The version parameter is not valid" +msgstr "O parâmetro da versão não é válido" + +#: taiga/projects/occ/mixins.py:74 +msgid "The version doesn't match with the current one" +msgstr "A versão não corresponde com a atual" + +#: taiga/projects/occ/mixins.py:93 +msgid "version" +msgstr "versão" + +#: taiga/projects/permissions.py:39 +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" + +#: taiga/projects/serializers.py:240 +msgid "Email address is already taken" +msgstr "Endereço de e-mail já utilizado" + +#: taiga/projects/serializers.py:252 +msgid "Invalid role for the project" +msgstr "Função inválida para projeto" + +#: taiga/projects/serializers.py:397 +msgid "Default options" +msgstr "Opções padrão" + +#: taiga/projects/serializers.py:398 +msgid "User story's statuses" +msgstr "Status de user story" + +#: taiga/projects/serializers.py:399 +msgid "Points" +msgstr "Pontos" + +#: taiga/projects/serializers.py:400 +msgid "Task's statuses" +msgstr "Status de tarefas" + +#: taiga/projects/serializers.py:401 +msgid "Issue's statuses" +msgstr "Status de casos" + +#: taiga/projects/serializers.py:402 +msgid "Issue's types" +msgstr "Tipos de casos" + +#: taiga/projects/serializers.py:403 +msgid "Priorities" +msgstr "Prioridades" + +#: taiga/projects/serializers.py:404 +msgid "Severities" +msgstr "Severidades" + +#: taiga/projects/serializers.py:405 +msgid "Roles" +msgstr "Funções" + +#: taiga/projects/services/stats.py:85 +msgid "Future sprint" +msgstr "Sprint futuro" + +#: taiga/projects/services/stats.py:102 +msgid "Project End" +msgstr "Fim do projeto" + +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +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:107 +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:110 +msgid "You don't have permissions to set this status to this task." +msgstr "Você não tem permissão para colocar esse status para essa tarefa." + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "ordenar por US" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "ordenar por quadro de tarefa" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "é Iocaine" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "Não há tarefas com esse id" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "alguém" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:11 +#, python-format +msgid "" +"\n" +"

You have been invited to Taiga!

\n" +"

Hi! %(full_name)s has sent you an invitation to join project " +"%(project)s in Taiga.
Taiga is a Free, open Source Agile Project " +"Management Tool.

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

Você foi convidado para o Taiga!

\n" +"

Oi! %(full_name)s te enviou um convite para se juntar ao projeto " +"%(project)s no Taiga.
Taiga é uma ferramenta de gerenciamento de " +"projetos ágil, código aberto e grátis.

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

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

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

E agora algumas palavras do bom companheiros ou companheiras
" +"que vieram tão gentilmente convidá-lo

\n" +"

%(extra)s

" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation to Taiga" +msgstr "Aceita seu convite para o Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation" +msgstr "Aceite seu convite" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "The Taiga Team" +msgstr "O Time Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:6 +#, python-format +msgid "" +"\n" +"You, or someone you know, has invited you to Taiga\n" +"\n" +"Hi! %(full_name)s has sent you an invitation to join a project called " +"%(project)s which is being managed on Taiga, a Free, open Source Agile " +"Project Management Tool.\n" +msgstr "" +"\n" +"Você, ou algum conhecido, convidou para o Taiga\n" +"\n" +"Oi! %(full_name)s te enviou um convite para se juntar ao projeto chamado " +"%(project)s que está começando a ser gerenciado no Taiga, Taiga é uma " +"ferramenta de gerenciamento de projetos ágil, código aberto e grátis.\n" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:12 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" +"\n" +"E agora algumas palavras do bom companheiro ou companheira que pensou tão " +"gentilmente como convidá-lo:\n" +"\n" +"%(extra)s\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:18 +msgid "Accept your invitation to Taiga following this link:" +msgstr "Aceite seu convite para o Taiga seguindo esse link:" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:20 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] convite para se juntar ao projeto '%(project)s'\n" + +#: taiga/projects/templates/emails/membership_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

You have been added to a project

\n" +"

Hello %(full_name)s,
you have been added to the project " +"%(project)s

\n" +" Go to " +"project\n" +"

The Taiga Team

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

Você foi adicionado ao projeto

\n" +"

Olá %(full_name)s,
você foi adicionado ao projeto %(project)s

\n" +"Ir ao " +"projeto\n" +"

O Time Taiga

\n" +" " + +#: taiga/projects/templates/emails/membership_notification-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"You have been added to a project\n" +"Hello %(full_name)s,you have been added to the project %(project)s\n" +"\n" +"See project at %(url)s\n" +msgstr "" +"\n" +"Você foi adicionado ao projeto\n" +"Olá %(full_name)s, você foi adicionado ao projeto %(project)s\n" +"\n" +"Ver projeto em %(url)s\n" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Adicionado ao projeto '%(project)s'\n" + +#. Translators: Name of scrum project template. +#: taiga/projects/translations.py:28 +msgid "Scrum" +msgstr "Scrum" + +#. Translators: Description of scrum project template. +#: taiga/projects/translations.py:30 +msgid "" +"The agile product backlog in Scrum is a prioritized features list, " +"containing short descriptions of all functionality desired in the product. " +"When applying Scrum, it's not necessary to start a project with a lengthy, " +"upfront effort to document all requirements. The Scrum product backlog is " +"then allowed to grow and change as more is learned about the product and its " +"customers" +msgstr "" +"O backlog no scrum é uma lista de funcionalidades priorizadas, contendo " +"pequenas descrições de todas as funcionalidades desejadas no produto. Quando " +"se aplicada ao scrum, não é necessário começar com um longo esforço inicial " +"para documentar todos os requisitos. O backlog permite crescer e modificar-" +"se no processo que é compreendido sobre o produto e seus clientes." + +#. Translators: Name of kanban project template. +#: taiga/projects/translations.py:33 +msgid "Kanban" +msgstr "Kanban" + +#. Translators: Description of kanban project template. +#: taiga/projects/translations.py:35 +msgid "" +"Kanban is a method for managing knowledge work with an emphasis on just-in-" +"time delivery while not overloading the team members. In this approach, the " +"process, from definition of a task to its delivery to the customer, is " +"displayed for participants to see and team members pull work from a queue." +msgstr "" +"Kanban é um método de gerenciar o trabalho conhecido com ênfase em entregas " +"just-in-time, não sobrecarregando membros dos times. Nessa abordagem, o " +"processo, da definição da tarefa até a entrega para o cliente, é exibida " +"para os participantes verem os próprios membros do time pegar o trabalho de " +"uma lista." + +#. Translators: User story point value (value = undefined) +#: taiga/projects/translations.py:43 +msgid "?" +msgstr "?" + +#. Translators: User story point value (value = 0) +#: taiga/projects/translations.py:45 +msgid "0" +msgstr "0" + +#. Translators: User story point value (value = 0.5) +#: taiga/projects/translations.py:47 +msgid "1/2" +msgstr "1/2" + +#. Translators: User story point value (value = 1) +#: taiga/projects/translations.py:49 +msgid "1" +msgstr "1" + +#. Translators: User story point value (value = 2) +#: taiga/projects/translations.py:51 +msgid "2" +msgstr "2" + +#. Translators: User story point value (value = 3) +#: taiga/projects/translations.py:53 +msgid "3" +msgstr "3" + +#. Translators: User story point value (value = 5) +#: taiga/projects/translations.py:55 +msgid "5" +msgstr "5" + +#. Translators: User story point value (value = 8) +#: taiga/projects/translations.py:57 +msgid "8" +msgstr "8" + +#. Translators: User story point value (value = 10) +#: taiga/projects/translations.py:59 +msgid "10" +msgstr "10" + +#. Translators: User story point value (value = 13) +#: taiga/projects/translations.py:61 +msgid "13" +msgstr "13" + +#. Translators: User story point value (value = 20) +#: taiga/projects/translations.py:63 +msgid "20" +msgstr "20" + +#. Translators: User story point value (value = 40) +#: taiga/projects/translations.py:65 +msgid "40" +msgstr "40" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:73 taiga/projects/translations.py:96 +#: taiga/projects/translations.py:112 +msgid "New" +msgstr "Novo" + +#. Translators: User story status +#: taiga/projects/translations.py:76 +msgid "Ready" +msgstr "Pronto" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:79 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 +msgid "In progress" +msgstr "Em andamento" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:82 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 +msgid "Ready for test" +msgstr "Pronto para teste" + +#. Translators: User story status +#: taiga/projects/translations.py:85 +msgid "Done" +msgstr "Terminado" + +#. Translators: User story status +#: taiga/projects/translations.py:88 +msgid "Archived" +msgstr "Arquivado" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:102 taiga/projects/translations.py:118 +msgid "Closed" +msgstr "Fechado" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 +msgid "Needs Info" +msgstr "Precisa de informação" + +#. Translators: Issue status +#: taiga/projects/translations.py:122 +msgid "Postponed" +msgstr "Adiado" + +#. Translators: Issue status +#: taiga/projects/translations.py:124 +msgid "Rejected" +msgstr "Rejeitado" + +#. Translators: Issue type +#: taiga/projects/translations.py:132 +msgid "Bug" +msgstr "Bug" + +#. Translators: Issue type +#: taiga/projects/translations.py:134 +msgid "Question" +msgstr "Pergunta" + +#. Translators: Issue type +#: taiga/projects/translations.py:136 +msgid "Enhancement" +msgstr "Melhoria" + +#. Translators: Issue priority +#: taiga/projects/translations.py:144 +msgid "Low" +msgstr "Baixa" + +#. Translators: Issue priority +#. Translators: Issue severity +#: taiga/projects/translations.py:146 taiga/projects/translations.py:159 +msgid "Normal" +msgstr "Normal" + +#. Translators: Issue priority +#: taiga/projects/translations.py:148 +msgid "High" +msgstr "Alta" + +#. Translators: Issue severity +#: taiga/projects/translations.py:155 +msgid "Wishlist" +msgstr "Desejável" + +#. Translators: Issue severity +#: taiga/projects/translations.py:157 +msgid "Minor" +msgstr "Secundário" + +#. Translators: Issue severity +#: taiga/projects/translations.py:161 +msgid "Important" +msgstr "Importante" + +#. Translators: Issue severity +#: taiga/projects/translations.py:163 +msgid "Critical" +msgstr "Crítica" + +#. Translators: User role +#: taiga/projects/translations.py:170 +msgid "UX" +msgstr "UX" + +#. Translators: User role +#: taiga/projects/translations.py:172 +msgid "Design" +msgstr "Design" + +#. Translators: User role +#: taiga/projects/translations.py:174 +msgid "Front" +msgstr "Front" + +#. Translators: User role +#: taiga/projects/translations.py:176 +msgid "Back" +msgstr "Back" + +#. Translators: User role +#: taiga/projects/translations.py:178 +msgid "Product Owner" +msgstr "Product Owner" + +#. Translators: User role +#: taiga/projects/translations.py:180 +msgid "Stakeholder" +msgstr "Stakeholder" + +#: taiga/projects/userstories/api.py:156 +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:160 +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:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" +msgstr "" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "função" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "ordem do backlog" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "ordem do sprint" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "data de término" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "É requerimento do cliente" + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "É requerimento do time" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "Gerado do caso" + +#: taiga/projects/userstories/validators.py:28 +msgid "There's no user story with that id" +msgstr "Não há user story com esse id" + +#: taiga/projects/validators.py:28 +msgid "There's no project with that id" +msgstr "Não há projeto com esse id" + +#: taiga/projects/validators.py:37 +msgid "There's no user story status with that id" +msgstr "Não há status de user story com este id" + +#: taiga/projects/validators.py:46 +msgid "There's no task status with that id" +msgstr "Não há status de tarega com este id" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:56 +msgid "Votes" +msgstr "Votos" + +#: taiga/projects/votes/models.py:55 +msgid "Vote" +msgstr "Vote" + +#: taiga/projects/wiki/api.py:66 +msgid "'content' parameter is mandatory" +msgstr "parâmetro 'conteúdo' é mandatório" + +#: taiga/projects/wiki/api.py:69 +msgid "'project_id' parameter is mandatory" +msgstr "parametro 'project_id' é mandatório" + +#: taiga/projects/wiki/models.py:37 +msgid "last modifier" +msgstr "último modificador" + +#: taiga/projects/wiki/models.py:70 +msgid "href" +msgstr "href" + +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "Verifique o histórico da API para a exata diferença" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "Informação pessoal" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "Permissões" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "Datas importantes" + +#: taiga/users/api.py:111 +msgid "Duplicated email" +msgstr "E-mail duplicado" + +#: taiga/users/api.py:113 +msgid "Not valid email" +msgstr "Não é um e-mail válido" + +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Usuário ou e-mail inválido" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "E-mail enviado com sucesso" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Token é inválido" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Parâmetro de senha atual necessário" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Parâmetro de nova senha necessário" + +#: taiga/users/api.py:205 +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:208 +msgid "Invalid current password" +msgstr "Senha atual inválida" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Argumentos incompletos" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Formato de imagem inválida" + +#: taiga/users/api.py:256 taiga/users/api.py:262 +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:289 taiga/users/api.py:297 taiga/users/api.py:300 +msgid "Invalid, are you sure the token is correct?" +msgstr "Inválido, tem certeza que o token está correto?" + +#: taiga/users/models.py:71 +msgid "superuser status" +msgstr "status de superuser" + +#: taiga/users/models.py:72 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" +"Designa que esse usuário tem todas as permissões sem explicitamente assiná-" +"las" + +#: taiga/users/models.py:102 +msgid "username" +msgstr "usuário" + +#: taiga/users/models.py:103 +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:106 +msgid "Enter a valid username." +msgstr "Digite um usuário válido" + +#: taiga/users/models.py:109 +msgid "active" +msgstr "ativo" + +#: taiga/users/models.py:110 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" +"Designa quando esse usuário deve ser tratado como ativo. desmarque isso em " +"vez de deletar contas." + +#: taiga/users/models.py:116 +msgid "biography" +msgstr "biografia" + +#: taiga/users/models.py:119 +msgid "photo" +msgstr "foto" + +#: taiga/users/models.py:120 +msgid "date joined" +msgstr "data ingressado" + +#: taiga/users/models.py:122 +msgid "default language" +msgstr "lingua padrão" + +#: taiga/users/models.py:124 +msgid "default theme" +msgstr "tema padrão" + +#: taiga/users/models.py:126 +msgid "default timezone" +msgstr "fuso horário padrão" + +#: taiga/users/models.py:128 +msgid "colorize tags" +msgstr "tags coloridas" + +#: taiga/users/models.py:133 +msgid "email token" +msgstr "token de e-mail" + +#: taiga/users/models.py:135 +msgid "new email address" +msgstr "novo endereço de email" + +#: taiga/users/models.py:203 +msgid "permissions" +msgstr "permissões" + +#: taiga/users/serializers.py:62 +msgid "invalid" +msgstr "inválido" + +#: taiga/users/serializers.py:73 +msgid "Invalid username. Try with a different one." +msgstr "Usuário inválido. Tente com um diferente." + +#: taiga/users/services.py:53 taiga/users/services.py:57 +msgid "Username or password does not matches user." +msgstr "Usuário ou senha não correspondem ao usuário" + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Mudar seu e-mail

\n" +"

Olá %(full_name)s,
por favor confirmar seu e-mail

\n" +" Confirmar e-mail\n" +"

Desconsidere essa mensagem se não solicitou.

\n" +"

O Time Taiga

\n" +" " + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Olá %(full_name)s, por favor confirmar seu e-mail\n" +"\n" +"%(url)s\n" +"\n" +"Você pode ignorar essa mensagem caso não tenha solicitado\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "[Taiga] Troca de email" + +#: taiga/users/templates/emails/password_recovery-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Recover your password

\n" +"

Hello %(full_name)s,
you asked to recover your password

\n" +" Recover your password\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Recuperar sua senha

\n" +"

Olá %(full_name)s,
você solicitou para recuperar sua senha

\n" +"Recuperar " +"sua senha\n" +"

Você pode ignorar essa mensagem se não solicitou.

\n" +"

O Time Taiga

\n" +" " + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Olá %(full_name)s, você solicitiou a alteração de sua senha\n" +"\n" +"%(url)s\n" +"\n" +"Você pode ignorar essa mensagem se não solicitou\n" +"\n" +"---\n" +"O Time Taiga\n" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "[Taiga] Recuperação de senha" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" \n" +" " +msgstr "" +"\n" +"\n" +"

Obrigado por se registrar no Taiga

\n" +"

Esperamos que você goste

\n" +"

Fizemos o taiga porque queriamos uma ferramenta de gerenciamento de " +"projetos que se colocasse aberta em nosso computadores durante o dia, que " +"nos lembrassem porque amamos colaborar, programar e projetar.

\n" +"

Construimos para ser bela, elegante, simples de usar e divertida - sem " +"abrir mão de flexibilidade e poder.

\n" +"O Time Taiga\n" +"\n" +" " + +#: taiga/users/templates/emails/registered_user-body-html.jinja:23 +#, python-format +msgid "" +"\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" +"\n" +" Você pode remover sua conta desse serviço clicando " +"aqui\n" +" " + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" +"\n" +"Obrigado por se registrar no Taiga\n" +"\n" +"Esperamos que você aproveite\n" +"\n" +"Fizemos o taiga porque queriamos uma ferramenta de gerenciamento de projetos " +"que se colocasse aberta em nosso computadores durante o dia, que nos " +"lembrassem porque amamos colaborar, programar e projetar.\n" +"\n" +"Construimos para ser bela, elegante, simples de usar e divertida - sem abrir " +"mão de flexibilidade e poder.\n" +"\n" +"--\n" +"O Time Taiga\n" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" +"\n" +"Você já pode remover sua conta desse serviço: %(url)s\n" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "Você foi Taigatizado!" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "Não há função com esse id" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" +"Valor de chave duplicada viola regra de limitação. Chave '{}' já existe." + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "chave" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "URL" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "chave secreta" + +#: taiga/webhooks/models.py:39 +msgid "status code" +msgstr "código de status" + +#: taiga/webhooks/models.py:40 +msgid "request data" +msgstr "dados da requisição" + +#: taiga/webhooks/models.py:41 +msgid "request headers" +msgstr "cabeçalhos da requisição" + +#: taiga/webhooks/models.py:42 +msgid "response data" +msgstr "dados de resposta" + +#: taiga/webhooks/models.py:43 +msgid "response headers" +msgstr "cabeçalhos de resposta" + +#: taiga/webhooks/models.py:44 +msgid "duration" +msgstr "duração" diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 00000000..8d75d1e5 --- /dev/null +++ b/taiga/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,3616 @@ +# taiga-back.taiga. +# Copyright (C) 2014-2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +# dmitriy , 2015 +# Dmitriy Volkov , 2015 +# Dmitry Lobanov , 2015 +# Dmitry Vinokurov , 2015 +# Марат , 2015 +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/" +"language/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: taiga/auth/api.py:99 +msgid "Public register is disabled." +msgstr "Публичная регистрация отключена." + +#: taiga/auth/api.py:132 +msgid "invalid register type" +msgstr "неправильный тип регистрации" + +#: taiga/auth/api.py:145 +msgid "invalid login type" +msgstr "неправильный тип логина" + +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 +msgid "invalid username" +msgstr "неправильное имя пользователя" + +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "Обязательно. 255 символов или меньше. Буквы, числа и символы /./-/_" + +#: taiga/auth/services.py:73 +msgid "Username is already in use." +msgstr "Это имя пользователя уже используется." + +#: taiga/auth/services.py:76 +msgid "Email is already in use." +msgstr "Этот адрес почты уже используется." + +#: taiga/auth/services.py:92 +msgid "Token not matches any valid invitation." +msgstr "Токен не подходит ни под одно корректное приглашение." + +#: taiga/auth/services.py:120 +msgid "User is already registered." +msgstr "Пользователь уже зарегистрирован." + +#: taiga/auth/services.py:144 +msgid "Membership with user is already exists." +msgstr "Членство с этим пользователем уже существует." + +#: taiga/auth/services.py:170 +msgid "Error on creating new user." +msgstr "Ошибка при создании нового пользователя." + +#: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 +msgid "Invalid token" +msgstr "Неверный токен" + +#: taiga/base/api/fields.py:268 +msgid "This field is required." +msgstr "Это поле обязательно." + +#: taiga/base/api/fields.py:269 taiga/base/api/relations.py:311 +msgid "Invalid value." +msgstr "Неправильное значение." + +#: taiga/base/api/fields.py:453 +#, python-format +msgid "'%s' value must be either True or False." +msgstr "значение '%s' должно быть True - верно - или False - ложно." + +#: taiga/base/api/fields.py:517 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "" +"Введите корректное 'ссылочное имя' состоящее из букв, чисел, подчёркиваний и " +"дефисов." + +#: taiga/base/api/fields.py:532 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" +"Выберите правильное значение. %(value)s не является одним из доступных " +"значений." + +#: taiga/base/api/fields.py:595 +msgid "Enter a valid email address." +msgstr "Введите правильный адрес email." + +#: taiga/base/api/fields.py:637 +#, python-format +msgid "Date has wrong format. Use one of these formats instead: %s" +msgstr "Дата имеет неверный формат. Воспользуйтесь одним из этих форматов: %s" + +#: taiga/base/api/fields.py:701 +#, python-format +msgid "Datetime has wrong format. Use one of these formats instead: %s" +msgstr "" +"Дата и время имеют неправильный формат. Воспользуйтесь одним из этих " +"форматов: %s" + +#: taiga/base/api/fields.py:771 +#, python-format +msgid "Time has wrong format. Use one of these formats instead: %s" +msgstr "" +"Время имеет неправильный формат. Воспользуйтесь одним из этих форматов: %s" + +#: taiga/base/api/fields.py:828 +msgid "Enter a whole number." +msgstr "Введите целое число." + +#: taiga/base/api/fields.py:829 taiga/base/api/fields.py:882 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "Убедитесь, что это значение меньше или равно %(limit_value)s." + +#: taiga/base/api/fields.py:830 taiga/base/api/fields.py:883 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "Убедитесь, что это значение больше или равно %(limit_value)s." + +#: taiga/base/api/fields.py:860 +#, python-format +msgid "\"%s\" value must be a float." +msgstr "\"%s\" значение должно быть числом с плавающей точкой." + +#: taiga/base/api/fields.py:881 +msgid "Enter a number." +msgstr "Введите число." + +#: taiga/base/api/fields.py:884 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Убедитесь, что здесь всего не больше %s цифр." + +#: taiga/base/api/fields.py:885 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Убедитесь, что здесь не больше %s цифр после точкой." + +#: taiga/base/api/fields.py:886 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Убедитесь, что здесь не больше %s цифр перед точкой." + +#: taiga/base/api/fields.py:953 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "Файл не был отправлен. Проверьте тип кодировки на форме." + +#: taiga/base/api/fields.py:954 +msgid "No file was submitted." +msgstr "Файл не был отправлен." + +#: taiga/base/api/fields.py:955 +msgid "The submitted file is empty." +msgstr "Отправленный файл пуст." + +#: taiga/base/api/fields.py:956 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "" +"Убедитесь, что имя этого файла имеет не больше %(max)d букв (сейчас - " +"%(length)d)." + +#: taiga/base/api/fields.py:957 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "Пожалуйста, или отправьте файл, или снимите флажок." + +#: taiga/base/api/fields.py:997 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Загрузите корректное изображение. Файл, который вы загрузили - либо не " +"изображение, либо не корректное изображение." + +#: taiga/base/api/pagination.py:115 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "Страница не является 'последней' и не может быть приведена к int." + +#: taiga/base/api/pagination.py:119 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Неправильная страница (%(page_number)s): %(message)s" + +#: taiga/base/api/permissions.py:61 +msgid "Invalid permission definition." +msgstr "Неправильное определение разрешения" + +#: taiga/base/api/relations.py:221 +#, python-format +msgid "Invalid pk '%s' - object does not exist." +msgstr "Неправильное значение ключа '%s' - объект не существует." + +#: taiga/base/api/relations.py:222 +#, python-format +msgid "Incorrect type. Expected pk value, received %s." +msgstr "Неверный тип. Ожидалось значение ключа, пришло %s." + +#: taiga/base/api/relations.py:310 +#, python-format +msgid "Object with %s=%s does not exist." +msgstr "Объект с %s=%s не существует." + +#: taiga/base/api/relations.py:346 +msgid "Invalid hyperlink - No URL match" +msgstr "Неправильная гиперссылка - нет подходящего URL" + +#: taiga/base/api/relations.py:347 +msgid "Invalid hyperlink - Incorrect URL match" +msgstr "Неправильная гиперссылка - URL не подходит" + +#: taiga/base/api/relations.py:348 +msgid "Invalid hyperlink due to configuration error" +msgstr "Неправильная гиперссылка из-за ошибки конфигурации" + +#: taiga/base/api/relations.py:349 +msgid "Invalid hyperlink - object does not exist." +msgstr "Неправильная ссылка - объект не существует." + +#: taiga/base/api/relations.py:350 +#, python-format +msgid "Incorrect type. Expected url string, received %s." +msgstr "Неверный тип. Ожидалась строка URL, получено %s." + +#: taiga/base/api/serializers.py:296 +msgid "Invalid data" +msgstr "Неправильные данные." + +#: taiga/base/api/serializers.py:388 +msgid "No input provided" +msgstr "Ввод отсутствует" + +#: taiga/base/api/serializers.py:548 +msgid "Cannot create a new item, only existing items may be updated." +msgstr "" +"Нельзя создать новые объект, только существующие объекты могут быть изменены." + +#: taiga/base/api/serializers.py:559 +msgid "Expected a list of items." +msgstr "Ожидался список объектов." + +#: taiga/base/api/views.py:100 +msgid "Not found" +msgstr "Не найдено" + +#: taiga/base/api/views.py:103 +msgid "Permission denied" +msgstr "Доступ запрещён" + +#: taiga/base/api/views.py:451 +msgid "Server application error" +msgstr "Ошибка приложения на сервере" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "Ошибка соединения." + +#: taiga/base/exceptions.py:53 +msgid "Malformed request." +msgstr "Неверное сформированный запрос." + +#: taiga/base/exceptions.py:58 +msgid "Incorrect authentication credentials." +msgstr "Неверные данные для аутентификации." + +#: taiga/base/exceptions.py:63 +msgid "Authentication credentials were not provided." +msgstr "Данные для аутентификации не предоставлены." + +#: taiga/base/exceptions.py:68 +msgid "You do not have permission to perform this action." +msgstr "У вас нет разрешения для этого действия." + +#: taiga/base/exceptions.py:73 +#, python-format +msgid "Method '%s' not allowed." +msgstr "Метод '%s' не разрешён." + +#: taiga/base/exceptions.py:81 +msgid "Could not satisfy the request's Accept header" +msgstr "Не удалось соответствовать заголовку принятия для этого запроса" + +#: taiga/base/exceptions.py:90 +#, python-format +msgid "Unsupported media type '%s' in request." +msgstr "Не поддерживаемый тип медиа '%s' в запросе." + +#: taiga/base/exceptions.py:98 +msgid "Request was throttled." +msgstr "Запрос был замят" + +#: taiga/base/exceptions.py:99 +#, python-format +msgid "Expected available in %d second%s." +msgstr "Будет доступно в течение %d секунд%s." + +#: taiga/base/exceptions.py:113 +msgid "Unexpected error" +msgstr "Неожиданная ошибка" + +#: taiga/base/exceptions.py:125 +msgid "Not found." +msgstr "Не найдено." + +#: taiga/base/exceptions.py:130 +msgid "Method not supported for this endpoint." +msgstr "Метод не поддерживается с этого конца." + +#: taiga/base/exceptions.py:138 taiga/base/exceptions.py:146 +msgid "Wrong arguments." +msgstr "Неправильные аргументы." + +#: taiga/base/exceptions.py:150 +msgid "Data validation error" +msgstr "Ошибка при проверке данных" + +#: taiga/base/exceptions.py:162 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "Ошибка целостности из-за неправильных параметров" + +#: taiga/base/exceptions.py:169 +msgid "Precondition error" +msgstr "Ошибка предусловия" + +#: taiga/base/filters.py:80 +msgid "Error in filter params types." +msgstr "Ошибка в типах фильтров для параметров." + +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 +msgid "'project' must be an integer value." +msgstr "'project' должно быть целым значением." + +#: taiga/base/tags.py:25 +msgid "tags" +msgstr "тэги" + +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "Taiga" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "Следите за нами в Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "Скачайте код на GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "Посетите наш вебсайт" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "Taiga.io" + +#: taiga/base/templates/emails/base-body-html.jinja:423 +#: taiga/base/templates/emails/hero-body-html.jinja:397 +#: taiga/base/templates/emails/updates-body-html.jinja:459 +#, python-format +msgid "" +"\n" +" Taiga Support:\n" +" %(support_url)s\n" +"
\n" +" Contact us:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Mailing list:\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " +msgstr "" +"\n" +" ПоддержкаTaiga:\n" +" %(support_url)s\n" +"
\n" +" Свяжитесь с нами:" +"\n" +"
\n" +" %(support_email)s\n" +" \n" +"
\n" +" Рассылка:\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Вы в Тайге" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

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

Вы в Тайге!

\n" +"

Добро пожаловать в Тайгу " +"- инструмент с открытым исходным кодом для управления проектами в стиле " +"Agile

\n" +" " + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "[Taiga] Обновления" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "Обновления" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

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

комментарий:" +"

\n" +"

" +"%(comment)s

\n" +" " + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" +"\n" +" Комментарий: %(comment)s\n" +" " + +#: taiga/export_import/api.py:103 +msgid "We needed at least one role" +msgstr "Нам была нужна хотя бы одна роль" + +#: taiga/export_import/api.py:197 +msgid "Needed dump file" +msgstr "Необходим дамп-файл" + +#: taiga/export_import/api.py:204 +msgid "Invalid dump format" +msgstr "Неправильный формат для свалки" + +#: taiga/export_import/dump_service.py:96 +msgid "error importing project data" +msgstr "ошибка импорта данных по проекту" + +#: taiga/export_import/dump_service.py:109 +msgid "error importing lists of project attributes" +msgstr "ошибка импорта списка атрибутов проекта" + +#: taiga/export_import/dump_service.py:114 +msgid "error importing default project attributes values" +msgstr "ошибка импорта значение по умолчанию для атрибутов проекта" + +#: taiga/export_import/dump_service.py:124 +msgid "error importing custom attributes" +msgstr "ошибка импорта специальных атрибутов" + +#: taiga/export_import/dump_service.py:129 +msgid "error importing roles" +msgstr "ошибка импорта ролей" + +#: taiga/export_import/dump_service.py:144 +msgid "error importing memberships" +msgstr "ошибка импорта членства" + +#: taiga/export_import/dump_service.py:149 +msgid "error importing sprints" +msgstr "ошибка импорта спринтов" + +#: taiga/export_import/dump_service.py:154 +msgid "error importing wiki pages" +msgstr "ошибка импорта вики-страниц" + +#: taiga/export_import/dump_service.py:159 +msgid "error importing wiki links" +msgstr "ошибка импорта вики-ссылок" + +#: taiga/export_import/dump_service.py:164 +msgid "error importing issues" +msgstr "ошибка импорта запросов" + +#: taiga/export_import/dump_service.py:169 +msgid "error importing user stories" +msgstr "ошибка импорта историй от пользователей" + +#: taiga/export_import/dump_service.py:174 +msgid "error importing tasks" +msgstr "ошибка импорта задач" + +#: taiga/export_import/dump_service.py:179 +msgid "error importing tags" +msgstr "ошибка импорта тэгов" + +#: taiga/export_import/dump_service.py:183 +msgid "error importing timelines" +msgstr "ошибка импорта хронологии проекта" + +#: taiga/export_import/serializers.py:163 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" не найдено в этом проекте" + +#: taiga/export_import/serializers.py:428 +#: taiga/projects/custom_attributes/serializers.py:103 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Неправильные данные. Должны быть в формате {\"key\": \"value\",...}" + +#: taiga/export_import/serializers.py:443 +#: taiga/projects/custom_attributes/serializers.py:118 +msgid "It contain invalid custom fields." +msgstr "Содержит неверные специальные поля" + +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 +msgid "Name duplicated for the project" +msgstr "Уже есть такое имя для проекта" + +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 +msgid "Error generating project dump" +msgstr "Ошибка создания свалочного файла для проекта" + +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 +msgid "Error loading project dump" +msgstr "Ошибка загрузки свалочного файла проекта" + +#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump generated

\n" +"

Hello %(user)s,

\n" +"

Your dump from project %(project)s has been correctly generated.\n" +"

You can download it here:

\n" +" Download the dump file\n" +"

This file will be deleted on %(deletion_date)s.

\n" +"

The Taiga Team

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

Создан дамп проекта

\n" +"

Здравствуйте, %(user)s,

\n" +"

Дамп проекта %(project)s успешно сгенерирован.

\n" +"

Вы можете скачать его здесь:

\n" +" Скачать " +"дамп\n" +"

Этот файл будет удалён %(deletion_date)s.

\n" +"

Команда Taiga

\n" +" " + +#: taiga/export_import/templates/emails/dump_project-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your dump from project %(project)s has been correctly generated. You can " +"download it here:\n" +"\n" +"%(url)s\n" +"\n" +"This file will be deleted on %(deletion_date)s.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Здравствуйте, %(user)s,\n" +"\n" +"Дамп для проекта %(project)s успешно сгенерирован. Вы можете скачать его " +"здесь:\n" +"\n" +"%(url)s\n" +"\n" +"Файл будет удалён %(deletion_date)s.\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "[%(project)s] Дамп вашего проекта успешно сгенерирован" + +#: taiga/export_import/templates/emails/export_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project %(project)s has not been exported correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"

Здравствуйте, %(user)s,

\n" +"

Ваш проект %(project)s не был корректно экспортирован.

\n" +"

Системные администраторы Taiga были проинформированы об этом.
" +"Пожалуйста, попробуйте ещё раз или свяжитесь со службой поддержки\n" +" %(support_email)s

\n" +"

Команда Taiga

\n" +" " + +#: taiga/export_import/templates/emails/export_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"Your project %(project)s has not been exported correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Здравствуйте, %(user)s,\n" +"\n" +"%(error_message)s\n" +"Ваш проект %(project)s не был корректно экспортирован.\n" +"\n" +"Системные администраторы Taiga уведомлены об этом.\n" +"\n" +"Пожалуйста, попробуйте ещё раз или свяжитесь со службой поддержки " +"%(support_email)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "[%(project)s] %(error_subject)s" + +#: taiga/export_import/templates/emails/import_error-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project has not been importer correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

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

%(error_message)s

\n" +"

Здравствуйте, %(user)s,

\n" +"

Ваш проект не был корректно импортирован.

\n" +"

Системные администраторы Taiga были проинформированы об этом.
" +"Пожалуйста, попробуйте ещё раз или свяжитесь со службой поддержки\n" +" %(support_email)s

\n" +"

Команда Taiga

" + +#: taiga/export_import/templates/emails/import_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Your project has not been importer correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Здравствуйте, %(user)s,\n" +"\n" +"%(error_message)s\n" +"Ваш проект не был корректно импортирован.\n" +"\n" +"Системные администраторы Taiga уведомлены об этом.\n" +"\n" +"Пожалуйста, попробуйте ещё раз или свяжитесь со службой поддержки " +"%(support_email)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "[Taiga] %(error_subject)s" + +#: taiga/export_import/templates/emails/load_dump-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump imported

\n" +"

Hello %(user)s,

\n" +"

Your project dump has been correctly imported.

\n" +" Go to %(project)s\n" +"

The Taiga Team

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

Дамп проекта импортирован

\n" +"

Здравствуйте, %(user)s,

\n" +"

Дамп вашего проекта успешно импортирован.

\n" +" Перейти к %(project)s\n" +"

Команда Taiga

\n" +" " + +#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your project dump has been correctly imported.\n" +"\n" +"You can see the project %(project)s here:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Здравствуйте, %(user)s,\n" +"\n" +"Дамп вашего проекта успешно импортирован.\n" +"\n" +"Вы можете посмотреть %(project)s здесь:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "[%(project)s] Дамп вашего проекта импортирован" + +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "Необходима аутентификация" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "имя" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "url иконки" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "веб" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "описание" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "Следующий url" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "секретный ключ для шифрования токенов приложения" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "пользователь" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "приложение" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 +msgid "full name" +msgstr "полное имя" + +#: taiga/feedback/models.py:25 taiga/users/models.py:108 +msgid "email address" +msgstr "адрес email" + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "комментарий" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 +msgid "created date" +msgstr "дата создания" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

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

Отзыв

\n" +"

Taiga получила отзывы от %(full_name)s <%(email)s>

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

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

Комментарий

\n" +"

%(comment)s

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "Дополнительное инфо" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" +"---------\n" +"- От: %(full_name)s <%(email)s>\n" +"---------\n" +"- Комментарий:\n" +"%(comment)s\n" +"---------" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "- Дополнительное инфо:" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" +"\n" +"[Taiga] Отзыв от %(full_name)s <%(email)s>\n" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "Нагрузочный файл не является правильным json-файлом" + +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 +msgid "The project doesn't exist" +msgstr "Проект не существует" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "Плохая подпись" + +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 +msgid "The referenced element doesn't exist" +msgstr "Указанный элемент не существует" + +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 +msgid "The status doesn't exist" +msgstr "Статус не существует" + +#: taiga/hooks/bitbucket/event_hooks.py:97 +msgid "Status changed from BitBucket commit" +msgstr "Статус изменён из-за вклада с BitBucket" + +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "Неверная информация о запросе" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Запрос создан [@{bitbucket_user_name}]({bitbucket_user_url} \"Посмотреть " +"профиль @{bitbucket_user_name} на BitBucket\") на BitBucket.\n" +"Изначальный запрос на BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Перейти к 'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "Запрос создан из BitBucket." + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "Неправильная информация в комментарии к запросу" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Комментарий от [@{bitbucket_user_name}]({bitbucket_user_url} \"Посмотреть " +"профиль @{bitbucket_user_name} на BitBucket\") на BitBucket.\n" +"Изначальный запрос на BitBucket: [bb#{number} - {subject}]({bitbucket_url} " +"\"Перейти к 'bb#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"Комментарий от BitBucket:\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 +#, python-brace-format +msgid "" +"Status changed by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +msgstr "" +"Статус изменён пользователем [@{github_user_name}]({github_user_url} " +"\"Посмотреть профиль @{github_user_name} на GitHub\") из-за вклада на GitHub " +"[{commit_id}]({commit_url} \"Посмотреть вклад '{commit_id} - " +"{commit_message}'\")." + +#: taiga/hooks/github/event_hooks.py:107 +msgid "Status changed from GitHub commit." +msgstr "Статус изменён из-за вклада на GitHub." + +#: taiga/hooks/github/event_hooks.py:157 +#, python-brace-format +msgid "" +"Issue created by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"Запрос создана [@{github_user_name}]({github_user_url} \"Посмотреть профиль " +"@{github_user_name} на GitHub\") из GitHub.\n" +"Исходный запрос на GitHub: [gh#{number} - {subject}]({github_url} \"Перейти " +"к 'gh#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/github/event_hooks.py:168 +msgid "Issue created from GitHub." +msgstr "Запрос создан из GitHub." + +#: taiga/hooks/github/event_hooks.py:200 +#, python-brace-format +msgid "" +"Comment by [@{github_user_name}]({github_user_url} \"See " +"@{github_user_name}'s GitHub profile\") from GitHub.\n" +"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " +"'gh#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Комментарий от [@{github_user_name}]({github_user_url} \"Посмотреть профиль " +"@{github_user_name} на GitHub\") из GitHub.\n" +"Исходный запрос на GitHub: [gh#{number} - {subject}]({github_url} \"Перейти " +"к 'gh#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:211 +#, python-brace-format +msgid "" +"Comment From GitHub:\n" +"\n" +"{message}" +msgstr "" +"Комментарий из GitHub:\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:86 +msgid "Status changed from GitLab commit" +msgstr "Статус изменён из-за вклада на GitLab" + +#: taiga/hooks/gitlab/event_hooks.py:128 +msgid "Created from GitLab" +msgstr "Создано из GitLab" + +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +"Комментарий от [@{gitlab_user_name}]({gitlab_user_url} \"Посмотреть профиль " +"@{gitlab_user_name} на GitLab\") из GitLab.\n" +"Исходный запрос на GitLab: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"Комментарий из GitLab:\n" +"\n" +"{message}" + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:51 +msgid "View project" +msgstr "Просмотреть проект" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:53 +msgid "View milestones" +msgstr "Просмотреть вехи" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "Просмотреть пользовательские истории" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 +msgid "View tasks" +msgstr "Просмотреть задачи" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:68 +msgid "View issues" +msgstr "Посмотреть запросы" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 +msgid "View wiki pages" +msgstr "Просмотреть wiki-страницы" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 +msgid "View wiki links" +msgstr "Просмотреть wiki-ссылки" + +#: taiga/permissions/permissions.py:38 +msgid "Request membership" +msgstr "Запросить членство" + +#: taiga/permissions/permissions.py:39 +msgid "Add user story to project" +msgstr "Добавить пользовательскую историю к проекту" + +#: taiga/permissions/permissions.py:40 +msgid "Add comments to user stories" +msgstr "Добавить комментарии к пользовательским историям" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to tasks" +msgstr "Добавить комментарии к задачам" + +#: taiga/permissions/permissions.py:42 +msgid "Add issues" +msgstr "Добавить запросы" + +#: taiga/permissions/permissions.py:43 +msgid "Add comments to issues" +msgstr "Добавить комментарии к запросам" + +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 +msgid "Add wiki page" +msgstr "Создать wiki-страницу" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 +msgid "Modify wiki page" +msgstr "Изменить wiki-страницу" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 +msgid "Add wiki link" +msgstr "Добавить wiki-ссылку" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 +msgid "Modify wiki link" +msgstr "Изменить wiki-ссылку" + +#: taiga/permissions/permissions.py:54 +msgid "Add milestone" +msgstr "Добавить веху" + +#: taiga/permissions/permissions.py:55 +msgid "Modify milestone" +msgstr "Изменить веху" + +#: taiga/permissions/permissions.py:56 +msgid "Delete milestone" +msgstr "Удалить веху" + +#: taiga/permissions/permissions.py:58 +msgid "View user story" +msgstr "Просмотреть пользовательскую историю" + +#: taiga/permissions/permissions.py:59 +msgid "Add user story" +msgstr "Добавить пользовательскую историю" + +#: taiga/permissions/permissions.py:60 +msgid "Modify user story" +msgstr "Изменить пользовательскую историю" + +#: taiga/permissions/permissions.py:61 +msgid "Delete user story" +msgstr "Удалить пользовательскую историю" + +#: taiga/permissions/permissions.py:64 +msgid "Add task" +msgstr "Добавить задачу" + +#: taiga/permissions/permissions.py:65 +msgid "Modify task" +msgstr "Изменить задачу" + +#: taiga/permissions/permissions.py:66 +msgid "Delete task" +msgstr "Удалить задачу" + +#: taiga/permissions/permissions.py:69 +msgid "Add issue" +msgstr "Добавить запрос" + +#: taiga/permissions/permissions.py:70 +msgid "Modify issue" +msgstr "Изменить запрос" + +#: taiga/permissions/permissions.py:71 +msgid "Delete issue" +msgstr "Удалить запрос" + +#: taiga/permissions/permissions.py:76 +msgid "Delete wiki page" +msgstr "Удалить wiki-страницу" + +#: taiga/permissions/permissions.py:81 +msgid "Delete wiki link" +msgstr "Удалить wiki-ссылку" + +#: taiga/permissions/permissions.py:85 +msgid "Modify project" +msgstr "Изменить проект" + +#: taiga/permissions/permissions.py:86 +msgid "Add member" +msgstr "Добавить участника" + +#: taiga/permissions/permissions.py:87 +msgid "Remove member" +msgstr "Удалить участника" + +#: taiga/permissions/permissions.py:88 +msgid "Delete project" +msgstr "Удалить проект" + +#: taiga/permissions/permissions.py:89 +msgid "Admin project values" +msgstr "Управлять значениями проекта" + +#: taiga/permissions/permissions.py:90 +msgid "Admin roles" +msgstr "Управлять ролями" + +#: taiga/projects/api.py:202 +msgid "Not valid template name" +msgstr "Неверное название шаблона" + +#: taiga/projects/api.py:205 +msgid "Not valid template description" +msgstr "Неверное описание шаблона" + +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 +msgid "At least one of the user must be an active admin" +msgstr "" +"По крайней мере один пользователь должен быть активным администратором." + +#: taiga/projects/api.py:511 +msgid "You don't have permisions to see that." +msgstr "У вас нет разрешения на просмотр." + +#: taiga/projects/attachments/api.py:47 +msgid "Partial updates are not supported" +msgstr "Частичные обновления не поддерживаются" + +#: taiga/projects/attachments/api.py:62 +msgid "Project ID not matches between object and project" +msgstr "Идентификатор проекта не подходит к этому объекту" + +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 +#: taiga/userstorage/models.py:25 +msgid "owner" +msgstr "владелец" + +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 +msgid "project" +msgstr "проект" + +#: taiga/projects/attachments/models.py:56 +msgid "content type" +msgstr "тип содержимого" + +#: taiga/projects/attachments/models.py:58 +msgid "object id" +msgstr "идентификатор объекта" + +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "изменённая дата" + +#: taiga/projects/attachments/models.py:69 +msgid "attached file" +msgstr "приложенный файл" + +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "sha1" + +#: taiga/projects/attachments/models.py:73 +msgid "is deprecated" +msgstr "устаревшее" + +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 +msgid "order" +msgstr "порядок" + +#: taiga/projects/choices.py:21 +msgid "AppearIn" +msgstr "AppearIn" + +#: taiga/projects/choices.py:22 +msgid "Jitsi" +msgstr "Jitsi" + +#: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "Специальный" + +#: taiga/projects/choices.py:24 +msgid "Talky" +msgstr "Talky" + +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "Текст" + +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "Многострочный текст" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "тип" + +#: taiga/projects/custom_attributes/models.py:87 +msgid "values" +msgstr "значения" + +#: taiga/projects/custom_attributes/models.py:97 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 +msgid "user story" +msgstr "пользовательская история" + +#: taiga/projects/custom_attributes/models.py:112 +msgid "task" +msgstr "задача" + +#: taiga/projects/custom_attributes/models.py:127 +msgid "issue" +msgstr "запрос" + +#: taiga/projects/custom_attributes/serializers.py:57 +msgid "Already exists one with the same name." +msgstr "Это имя уже используется." + +#: taiga/projects/history/api.py:70 +msgid "Comment already deleted" +msgstr "Комментарий уже был удалён" + +#: taiga/projects/history/api.py:89 +msgid "Comment not deleted" +msgstr "Комментарий не удалён" + +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "Изменить" + +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "Создать" + +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "Удалить" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "очки для роли %(role)s" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:193 +msgid "from" +msgstr "от" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:199 +msgid "to" +msgstr "кому" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "Добавлено новое вложение" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "Вложение обновлено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "устаревшее" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "не устаревшее" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "Удалённое вложение" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "добавлено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "удалено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 +msgid "Unassigned" +msgstr "Не назначено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:211 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "-удалено-" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "кому:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "от:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "Добавлено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "Изменено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "Удалено" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "добавлено:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "удалено:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "От:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "Кому:" + +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 +msgid "content" +msgstr "содержимое" + +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "Заметка о блокировке" + +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "спринт" + +#: taiga/projects/issues/api.py:160 +msgid "You don't have permissions to set this sprint to this issue." +msgstr "" +"У вас нет прав для того чтобы установить такой спринт для этого запроса" + +#: taiga/projects/issues/api.py:164 +msgid "You don't have permissions to set this status to this issue." +msgstr "" +"У вас нет прав для того чтобы установить такой статус для этого запроса" + +#: taiga/projects/issues/api.py:168 +msgid "You don't have permissions to set this severity to this issue." +msgstr "" +"У вас нет прав для того чтобы установить такую важность для этого запроса" + +#: taiga/projects/issues/api.py:172 +msgid "You don't have permissions to set this priority to this issue." +msgstr "" +"У вас нет прав для того чтобы установить такой приоритет для этого запроса" + +#: taiga/projects/issues/api.py:176 +msgid "You don't have permissions to set this type to this issue." +msgstr "У вас нет прав для того чтобы установить такой тип для этого запроса" + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "Ссылка" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "cтатус" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "важность" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "приоритет" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "веха" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "дата завершения" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "тема" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "назначено" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "внешняя ссылка" + +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "количество" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 +msgid "slug" +msgstr "ссылочное имя" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "предполагаемая дата начала" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "предполагаемая дата завершения" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 +msgid "is closed" +msgstr "закрыто" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "доступность" + +#: taiga/projects/milestones/models.py:75 +msgid "The estimated start must be previous to the estimated finish." +msgstr "" +"Предполагаемая дата начала должна предшествовать предполагаемой дате " +"завершения." + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "Не существует спринта с таким идентификатором" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "заблокировано" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "'{param}' parameter is mandatory" +msgstr "параметр '{param}' является обязательным" + +#: taiga/projects/mixins/ordering.py:51 +msgid "'project' parameter is mandatory" +msgstr "параметр 'project' является обязательным" + +#: taiga/projects/models.py:66 +msgid "email" +msgstr "электронная почта" + +#: taiga/projects/models.py:68 +msgid "create at" +msgstr "создано" + +#: taiga/projects/models.py:70 taiga/users/models.py:130 +msgid "token" +msgstr "идентификатор" + +#: taiga/projects/models.py:76 +msgid "invitation extra text" +msgstr "дополнительный текст к приглашению" + +#: taiga/projects/models.py:79 +msgid "user order" +msgstr "порядок пользователей" + +#: taiga/projects/models.py:89 +msgid "The user is already member of the project" +msgstr "Этот пользователем уже является участником проекта" + +#: taiga/projects/models.py:104 +msgid "default points" +msgstr "очки по умолчанию" + +#: taiga/projects/models.py:108 +msgid "default US status" +msgstr "статусы ПИ по умолчанию" + +#: taiga/projects/models.py:112 +msgid "default task status" +msgstr "статус задачи по умолчанию" + +#: taiga/projects/models.py:115 +msgid "default priority" +msgstr "приоритет по умолчанию" + +#: taiga/projects/models.py:118 +msgid "default severity" +msgstr "важность по умолчанию" + +#: taiga/projects/models.py:122 +msgid "default issue status" +msgstr "статус запроса по умолчанию" + +#: taiga/projects/models.py:126 +msgid "default issue type" +msgstr "тип запроса по умолчанию" + +#: taiga/projects/models.py:147 +msgid "members" +msgstr "участники" + +#: taiga/projects/models.py:150 +msgid "total of milestones" +msgstr "общее количество вех" + +#: taiga/projects/models.py:151 +msgid "total story points" +msgstr "очки истории" + +#: taiga/projects/models.py:154 taiga/projects/models.py:614 +msgid "active backlog panel" +msgstr "активная панель списка задач" + +#: taiga/projects/models.py:156 taiga/projects/models.py:616 +msgid "active kanban panel" +msgstr "активная панель kanban" + +#: taiga/projects/models.py:158 taiga/projects/models.py:618 +msgid "active wiki panel" +msgstr "активная wiki-панель" + +#: taiga/projects/models.py:160 taiga/projects/models.py:620 +msgid "active issues panel" +msgstr "панель активных запросов" + +#: taiga/projects/models.py:163 taiga/projects/models.py:623 +msgid "videoconference system" +msgstr "система видеоконференций" + +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "дополнительные данные системы видеоконференций" + +#: taiga/projects/models.py:170 +msgid "creation template" +msgstr "шаблон для создания" + +#: taiga/projects/models.py:173 +msgid "anonymous permissions" +msgstr "права анонимов" + +#: taiga/projects/models.py:177 +msgid "user permissions" +msgstr "права пользователя" + +#: taiga/projects/models.py:180 +msgid "is private" +msgstr "личное" + +#: taiga/projects/models.py:191 +msgid "tags colors" +msgstr "цвета тэгов" + +#: taiga/projects/models.py:383 +msgid "modules config" +msgstr "конфигурация модулей" + +#: taiga/projects/models.py:402 +msgid "is archived" +msgstr "архивировано" + +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 +msgid "color" +msgstr "цвет" + +#: taiga/projects/models.py:406 +msgid "work in progress limit" +msgstr "ограничение на активную работу" + +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 +msgid "value" +msgstr "значение" + +#: taiga/projects/models.py:611 +msgid "default owner's role" +msgstr "роль владельца по умолчанию" + +#: taiga/projects/models.py:627 +msgid "default options" +msgstr "параметры по умолчанию" + +#: taiga/projects/models.py:628 +msgid "us statuses" +msgstr "статусы ПИ" + +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 +msgid "points" +msgstr "очки" + +#: taiga/projects/models.py:630 +msgid "task statuses" +msgstr "статусы задач" + +#: taiga/projects/models.py:631 +msgid "issue statuses" +msgstr "статусы запросов" + +#: taiga/projects/models.py:632 +msgid "issue types" +msgstr "типы запросов" + +#: taiga/projects/models.py:633 +msgid "priorities" +msgstr "приоритеты" + +#: taiga/projects/models.py:634 +msgid "severities" +msgstr "степени важности" + +#: taiga/projects/models.py:635 +msgid "roles" +msgstr "роли" + +#: taiga/projects/notifications/choices.py:28 +msgid "Involved" +msgstr "" + +#: taiga/projects/notifications/choices.py:29 +msgid "All" +msgstr "" + +#: taiga/projects/notifications/choices.py:30 +msgid "None" +msgstr "" + +#: taiga/projects/notifications/models.py:61 +msgid "created date time" +msgstr "дата и время создания" + +#: taiga/projects/notifications/models.py:63 +msgid "updated date time" +msgstr "дата и время обновления" + +#: taiga/projects/notifications/models.py:65 +msgid "history entries" +msgstr "записи истории" + +#: taiga/projects/notifications/models.py:68 +msgid "notify users" +msgstr "уведомить пользователей" + +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "Просмотренные" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 +msgid "Notify exists for specified user and project" +msgstr "Уведомление существует для данных пользователя и проекта" + +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "Неверное значение для уровня уведомлений" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue updated

\n" +"

Hello %(user)s,
%(changer)s has updated an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +" " +msgstr "" +"\n" +"

Запрос обновлён

\n" +"

Привет %(user)s,
%(changer)s обновил(а) запрос в %(project)s\n" +"

Запрос #%(ref)s %(subject)s

\n" +" Просмотреть запрос\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Issue updated\n" +"Hello %(user)s, %(changer)s has updated an issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Запрос обновлён\n" +"Привет %(user)s, %(changer)s обновил(а) запрос %(project)s\n" +"Просмотреть запрос #%(ref)s %(subject)s можно по ссылке %(url)s\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Обновлён запрос #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New issue created

\n" +"

Hello %(user)s,
%(changer)s has created a new issue on " +"%(project)s

\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +"

The Taiga Team

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

Добавлен новый запрос

\n" +"

Привет %(user)s,
%(changer)s добавил(а) новый запрос в " +"%(project)s

\n" +"

Запрос #%(ref)s %(subject)s

\n" +" Просмотреть запрос\n" +"

Команда Тайги

\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New issue created\n" +"Hello %(user)s, %(changer)s has created a new issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Добавлен новый запрос\n" +"Привет %(user)s, %(changer)s добавил(а) новый запрос в %(project)s\n" +"Просмотреть запрос #%(ref)s %(subject)s можно по ссылке %(url)s\n" +"\n" +"---\n" +"Команда Тайги\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Добавлен запрос #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Запрос удалён

\n" +"

Привет %(user)s,
%(changer)s удалил(а) запрос из %(project)s\n" +"

Запрос #%(ref)s %(subject)s

\n" +"

Команда Тайги

\n" +" " + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Issue deleted\n" +"Hello %(user)s, %(changer)s has deleted an issue on %(project)s\n" +"Issue #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Запрос удалён\n" +"Привет %(user)s, %(changer)s удалил(а) запрос из %(project)s\n" +"Запрос #%(ref)s %(subject)s\n" +"\n" +"---\n" +"Команда Тайги\n" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Удалён запрос #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint updated

\n" +"

Hello %(user)s,
%(changer)s has updated an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See sprint\n" +" " +msgstr "" +"\n" +"

Спринт обновлён

\n" +"

Привет %(user)s,
%(changer)s обновил(а) спринт в %(project)s\n" +"

Спринт %(name)s

\n" +" Просмотреть спринт\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Sprint updated\n" +"Hello %(user)s, %(changer)s has updated a sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +msgstr "" +"\n" +"Спринт обновлён\n" +"Привет %(user)s, %(changer)s изменил(а) спринт в %(project)s\n" +"Просмотреть спринт %(name)s можно по ссылке %(url)s\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Обновлён спринт \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New sprint created

\n" +"

Hello %(user)s,
%(changer)s has created a new sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See " +"sprint\n" +"

The Taiga Team

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

Добавлен новый спринт

\n" +"

Привет %(user)s,
%(changer)s добавил(а) новый спринт к " +"%(project)s

\n" +"

Спринт %(name)s

\n" +" Просмотреть спринт\n" +"

Команда Тайги

\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New sprint created\n" +"Hello %(user)s, %(changer)s has created a new sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Добавлен новый спринт\n" +"Привет %(user)s, %(changer)s добавил(а) новый спринт к %(project)s\n" +"Просмотреть спринт %(name)s можно по ссылке %(url)s\n" +"\n" +"---\n" +"Команда Тайги\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Добавлен спринт \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +"

The Taiga Team

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

Спринт удалён

\n" +"

Привет %(user)s,
%(changer)s удалил(а) спринт из %(project)s\n" +"

Спринт %(name)s

\n" +"

Команда Тайги

\n" +" " + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Sprint deleted\n" +"Hello %(user)s, %(changer)s has deleted an sprint on %(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Спринт удалён\n" +"Привет %(user)s, %(changer)s удалил(а) спринт из %(project)s\n" +"Спринт %(name)s\n" +"\n" +"---\n" +"Команда Тайги\n" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n" +msgstr "" +"\n" +"[%(project)s] Удалён спринт \"%(milestone)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task updated

\n" +"

Hello %(user)s,
%(changer)s has updated a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +" " +msgstr "" +"\n" +"

Задача обновлена

\n" +"

Привет %(user)s,
%(changer)s обновил(а) задачу в %(project)s\n" +"

Задача #%(ref)s %(subject)s

\n" +" Просмотреть задачу\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Task updated\n" +"Hello %(user)s, %(changer)s has updated a task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"Задача обновлена\n" +"Привет %(user)s, %(changer)s обновил(а) задачу в %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Обновлена задача #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New task created

\n" +"

Hello %(user)s,
%(changer)s has created a new task on " +"%(project)s

\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +"

The Taiga Team

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

Новая задача создана

\n" +"

Здравствуйте, %(user)s,
%(changer)s создал новую задачу в " +"%(project)s

\n" +"

Задача #%(ref)s %(subject)s

\n" +" Посмотреть задачу\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New task created\n" +"Hello %(user)s, %(changer)s has created a new task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Новая задача создана\n" +"Здравствуйте, %(user)s, %(changer)s создал новую задачу в %(project)s\n" +"Посмотреть задачу #%(ref)s %(subject)s здесь: %(url)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Создана задача #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

Задача удалена

\n" +"

Здравствуйте, %(user)s,
%(changer)s удалил задачу для " +"%(project)s

\n" +"

Задача #%(ref)s %(subject)s

\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Task deleted\n" +"Hello %(user)s, %(changer)s has deleted a task on %(project)s\n" +"Task #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Задача удалена\n" +"Здравствуйте, %(user)s, %(changer)s удалил задачу для %(project)s\n" +"Задача #%(ref)s %(subject)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Удалена задача #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story updated

\n" +"

Hello %(user)s,
%(changer)s has updated a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +" " +msgstr "" +"\n" +"

История от Пользователя изменена

\n" +"

Здравствуйте, %(user)s,
%(changer)s обновил историю для " +"%(project)s

\n" +"

История от Пользователя #%(ref)s %(subject)s

\n" +" Посмотреть историю\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"User story updated\n" +"Hello %(user)s, %(changer)s has updated a user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +msgstr "" +"\n" +"История от пользователя изменена\n" +"Здравствуйте, %(user)s, %(changer)s обновил историю для %(project)s\n" +"Посмотреть историю от пользователя #%(ref)s %(subject)s здесь: %(url)s\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Изменена ПИ #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New user story created

\n" +"

Hello %(user)s,
%(changer)s has created a new user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +"

The Taiga Team

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

Новая история от пользователя создана

\n" +"

Здравствуйте, %(user)s,
%(changer)s создал новую историю для " +"%(project)s

\n" +"

История от Пользователя #%(ref)s %(subject)s

\n" +" Посмотреть историю\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New user story created\n" +"Hello %(user)s, %(changer)s has created a new user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Создана новая История от Пользователя\n" +"Здравствуйте, %(user)s, %(changer)s создал новую историю для %(project)s\n" +"Посмотреть историю от пользователя #%(ref)s %(subject)s здесь: %(url)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Создана ПИ #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +"

The Taiga Team

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

История от Пользователя удалена

\n" +"

Здравствуйте, %(user)s,
%(changer)s удалил историю для " +"%(project)s

\n" +"

История от пользователя #%(ref)s %(subject)s

\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"User Story deleted\n" +"Hello %(user)s, %(changer)s has deleted a user story on %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"История от Пользователя удалена\n" +"Здравствуйте, %(user)s, %(changer)s удалил историю для %(project)s\n" +"История от Пользователя #%(ref)s %(subject)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the US #%(ref)s \"%(subject)s\"\n" +msgstr "" +"\n" +"[%(project)s] Удалена ПИ #%(ref)s \"%(subject)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki Page updated

\n" +"

Hello %(user)s,
%(changer)s has updated a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See Wiki Page\n" +" " +msgstr "" +"\n" +"

Изменена вики-страница

\n" +"

Здравствуйте, %(user)s,
%(changer)s изменил вики-страницу " +"%(project)s

\n" +"

Вики-страница %(page)s

\n" +" Посмотреть вики-страницу\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Wiki Page updated\n" +"\n" +"Hello %(user)s, %(changer)s has updated a wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +msgstr "" +"\n" +"Вики-страница изменена\n" +"\n" +"Здравствуйте, %(user)s, %(changer)s изменил вики-страницу в %(project)s\n" +"\n" +"Просмотреть вики-страницу %(page)s можно по ссылке %(url)s\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Обновлена вики-страница \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New wiki page created

\n" +"

Hello %(user)s,
%(changer)s has created a new wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See " +"wiki page\n" +"

The Taiga Team

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

Создана новая вики-страница

\n" +"

Здравствуйте, %(user)s,
%(changer)s создал новую вики-страницу " +"для %(project)s

\n" +"

Вики-страница %(page)s

\n" +" Посмотреть вики-страницу\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New wiki page created\n" +"\n" +"Hello %(user)s, %(changer)s has created a new wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Создана новая вики-страница\n" +"\n" +"Здравствуйте, %(user)s, %(changer)s создал новую вики-страницу для " +"%(project)s\n" +"\n" +"Посмотреть вики-страницу %(page)s здесь: %(url)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Создана вики-страница \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki page deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +"

The Taiga Team

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

Вики-страница удалена

\n" +"

Здравствуйте, %(user)s,
%(changer)s удалил вики-страницу для " +"%(project)s

\n" +"

Вики-страница %(page)s

\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Wiki page deleted\n" +"\n" +"Hello %(user)s, %(changer)s has deleted a wiki page on %(project)s\n" +"\n" +"Wiki page %(page)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Вики-страница удалена\n" +"\n" +"Здравствуйте, %(user)s, %(changer)s удалил вики-страницу для %(project)s\n" +"\n" +"Вики-страница %(page)s\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Удалена вики-страница \"%(page)s\"\n" + +#: taiga/projects/notifications/validators.py:46 +msgid "Watchers contains invalid users" +msgstr "наблюдатели содержат неправильных пользователей" + +#: taiga/projects/occ/mixins.py:35 +msgid "The version must be an integer" +msgstr "Версия должна быть целым значением" + +#: taiga/projects/occ/mixins.py:58 +msgid "The version parameter is not valid" +msgstr "Значение версии некорректно" + +#: taiga/projects/occ/mixins.py:74 +msgid "The version doesn't match with the current one" +msgstr "Версия не соответствует текущей" + +#: taiga/projects/occ/mixins.py:93 +msgid "version" +msgstr "версия" + +#: taiga/projects/permissions.py:39 +msgid "You can't leave the project if there are no more owners" +msgstr "Вы не можете покинуть проект если в нём нет других владельцев" + +#: taiga/projects/serializers.py:240 +msgid "Email address is already taken" +msgstr "Этот почтовый адрес уже используется" + +#: taiga/projects/serializers.py:252 +msgid "Invalid role for the project" +msgstr "Неверная роль для этого проекта" + +#: taiga/projects/serializers.py:397 +msgid "Default options" +msgstr "Параметры по умолчанию" + +#: taiga/projects/serializers.py:398 +msgid "User story's statuses" +msgstr "Статусу пользовательских историй" + +#: taiga/projects/serializers.py:399 +msgid "Points" +msgstr "Очки" + +#: taiga/projects/serializers.py:400 +msgid "Task's statuses" +msgstr "Статусы задачи" + +#: taiga/projects/serializers.py:401 +msgid "Issue's statuses" +msgstr "Статусы запроса" + +#: taiga/projects/serializers.py:402 +msgid "Issue's types" +msgstr "Типы запроса" + +#: taiga/projects/serializers.py:403 +msgid "Priorities" +msgstr "Приоритеты" + +#: taiga/projects/serializers.py:404 +msgid "Severities" +msgstr "Степени важности" + +#: taiga/projects/serializers.py:405 +msgid "Roles" +msgstr "Роли" + +#: taiga/projects/services/stats.py:85 +msgid "Future sprint" +msgstr "Будущий спринт" + +#: taiga/projects/services/stats.py:102 +msgid "Project End" +msgstr "Окончание проекта" + +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "У вас нет прав, чтобы назначить этот спринт для этой задачи." + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "" +"У вас нет прав, чтобы назначить эту историю от пользователя этой задаче." + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." +msgstr "У вас нет прав, чтобы установить этот статус для этой задачи." + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "порядок ПИ" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "порядок панели задач" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "- иокаин" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "Нет задачи с таким идентификатором" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "некто" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:11 +#, python-format +msgid "" +"\n" +"

You have been invited to Taiga!

\n" +"

Hi! %(full_name)s has sent you an invitation to join project " +"%(project)s in Taiga.
Taiga is a Free, open Source Agile Project " +"Management Tool.

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

Вас пригласили в Taiga!

\n" +"

Привет! %(full_name)s пригласил вас присоединиться к проекту " +"%(project)s в Taiga.
Taiga - это бесплатный инструмент с открытым " +"исходным кодом для управления проектами в стиле Agile.

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

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

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

А теперь несколько слов от добрых братьев или сестёр,
" +"которые были столь любезны, что пригласили вас

\n" +"

%(extra)s

\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation to Taiga" +msgstr "Принять приглашение в Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation" +msgstr "Принять приглашение" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "The Taiga Team" +msgstr "Команда Тайги" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:6 +#, python-format +msgid "" +"\n" +"You, or someone you know, has invited you to Taiga\n" +"\n" +"Hi! %(full_name)s has sent you an invitation to join a project called " +"%(project)s which is being managed on Taiga, a Free, open Source Agile " +"Project Management Tool.\n" +msgstr "" +"\n" +"Вы, или кто-то, кого вы знаете, пригласили вас в Taiga\n" +"\n" +"Привет! %(full_name)s пригласили вас поучаствовать в проекте %(project)s, " +"который управляется в Taiga - бесплатном инструменте с открытым исходным " +"кодом для управления проектами в стиле Agile.\n" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:12 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" +"\n" +"А теперь несколько слов от добрых братьев или сестёр, которые были столь " +"любезны, что пригласили вас:\n" +"\n" +"%(extra)s\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:18 +msgid "Accept your invitation to Taiga following this link:" +msgstr "Принять приглашение в Тайгу можно перейдя по ссылке:" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:20 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"---\n" +"Команда Тайги\n" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Приглашение присоединиться к проекту '%(project)s'\n" + +#: taiga/projects/templates/emails/membership_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

You have been added to a project

\n" +"

Hello %(full_name)s,
you have been added to the project " +"%(project)s

\n" +" Go to " +"project\n" +"

The Taiga Team

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

Вы были добавлены в проект

\n" +"

Здравствуйте, %(full_name)s,
вы были добавлены в проект " +"%(project)s

\n" +" Перейти к проекту\n" +"

Команда Taiga

\n" +" " + +#: taiga/projects/templates/emails/membership_notification-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"You have been added to a project\n" +"Hello %(full_name)s,you have been added to the project %(project)s\n" +"\n" +"See project at %(url)s\n" +msgstr "" +"\n" +"Вы были добавлены в проект\n" +"Здравствуйте, %(full_name)s, вы были добавлены в проект %(project)s\n" +"\n" +"Посмотреть проект можно здесь: %(url)s\n" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Добавлены к проекту '%(project)s'\n" + +#. Translators: Name of scrum project template. +#: taiga/projects/translations.py:28 +msgid "Scrum" +msgstr "Scrum" + +#. Translators: Description of scrum project template. +#: taiga/projects/translations.py:30 +msgid "" +"The agile product backlog in Scrum is a prioritized features list, " +"containing short descriptions of all functionality desired in the product. " +"When applying Scrum, it's not necessary to start a project with a lengthy, " +"upfront effort to document all requirements. The Scrum product backlog is " +"then allowed to grow and change as more is learned about the product and its " +"customers" +msgstr "" +"Agile бэклог продукта в Scrum - это приоритезированный список пожеланий, " +"содержащий короткие описания всего функционала который должен быть в " +"продукте. При использовании Scrum, нет нужды начинать проект с длинного, " +"заранее составленного списка абсолютно всех требований. Scrum бэклог " +"продукта может расти и изменяться в ходе того как становится всё больше " +"известно о продукте и его пользователях." + +#. Translators: Name of kanban project template. +#: taiga/projects/translations.py:33 +msgid "Kanban" +msgstr "Kanban" + +#. Translators: Description of kanban project template. +#: taiga/projects/translations.py:35 +msgid "" +"Kanban is a method for managing knowledge work with an emphasis on just-in-" +"time delivery while not overloading the team members. In this approach, the " +"process, from definition of a task to its delivery to the customer, is " +"displayed for participants to see and team members pull work from a queue." +msgstr "" +"Kanban - это метод организации работы со знаниями, особое внимание уделяющий " +"моментальной передаче информации без лишней нагрузки членов команды. При " +"этом подходе весь процесс, от создания задачи до её отправки заказчику, " +"отображается для участников в удобном виде и члены команды могут брать себе " +"задачи из очереди." + +#. Translators: User story point value (value = undefined) +#: taiga/projects/translations.py:43 +msgid "?" +msgstr "?" + +#. Translators: User story point value (value = 0) +#: taiga/projects/translations.py:45 +msgid "0" +msgstr "0" + +#. Translators: User story point value (value = 0.5) +#: taiga/projects/translations.py:47 +msgid "1/2" +msgstr "1/2" + +#. Translators: User story point value (value = 1) +#: taiga/projects/translations.py:49 +msgid "1" +msgstr "1" + +#. Translators: User story point value (value = 2) +#: taiga/projects/translations.py:51 +msgid "2" +msgstr "2" + +#. Translators: User story point value (value = 3) +#: taiga/projects/translations.py:53 +msgid "3" +msgstr "3" + +#. Translators: User story point value (value = 5) +#: taiga/projects/translations.py:55 +msgid "5" +msgstr "5" + +#. Translators: User story point value (value = 8) +#: taiga/projects/translations.py:57 +msgid "8" +msgstr "8" + +#. Translators: User story point value (value = 10) +#: taiga/projects/translations.py:59 +msgid "10" +msgstr "10" + +#. Translators: User story point value (value = 13) +#: taiga/projects/translations.py:61 +msgid "13" +msgstr "13" + +#. Translators: User story point value (value = 20) +#: taiga/projects/translations.py:63 +msgid "20" +msgstr "20" + +#. Translators: User story point value (value = 40) +#: taiga/projects/translations.py:65 +msgid "40" +msgstr "40" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:73 taiga/projects/translations.py:96 +#: taiga/projects/translations.py:112 +msgid "New" +msgstr "Новая" + +#. Translators: User story status +#: taiga/projects/translations.py:76 +msgid "Ready" +msgstr "Готово" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:79 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 +msgid "In progress" +msgstr "В процессе" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:82 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 +msgid "Ready for test" +msgstr "Можно проверять" + +#. Translators: User story status +#: taiga/projects/translations.py:85 +msgid "Done" +msgstr "Завершена" + +#. Translators: User story status +#: taiga/projects/translations.py:88 +msgid "Archived" +msgstr "Архивирована" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:102 taiga/projects/translations.py:118 +msgid "Closed" +msgstr "Закрыта" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 +msgid "Needs Info" +msgstr "Требуются подробности" + +#. Translators: Issue status +#: taiga/projects/translations.py:122 +msgid "Postponed" +msgstr "Отложено" + +#. Translators: Issue status +#: taiga/projects/translations.py:124 +msgid "Rejected" +msgstr "Отклонена" + +#. Translators: Issue type +#: taiga/projects/translations.py:132 +msgid "Bug" +msgstr "Ошибка" + +#. Translators: Issue type +#: taiga/projects/translations.py:134 +msgid "Question" +msgstr "Вопрос" + +#. Translators: Issue type +#: taiga/projects/translations.py:136 +msgid "Enhancement" +msgstr "Улучшение" + +#. Translators: Issue priority +#: taiga/projects/translations.py:144 +msgid "Low" +msgstr "Низкий" + +#. Translators: Issue priority +#. Translators: Issue severity +#: taiga/projects/translations.py:146 taiga/projects/translations.py:159 +msgid "Normal" +msgstr "Обычный" + +#. Translators: Issue priority +#: taiga/projects/translations.py:148 +msgid "High" +msgstr "Высокий" + +#. Translators: Issue severity +#: taiga/projects/translations.py:155 +msgid "Wishlist" +msgstr "Список пожеланий" + +#. Translators: Issue severity +#: taiga/projects/translations.py:157 +msgid "Minor" +msgstr "Низкий" + +#. Translators: Issue severity +#: taiga/projects/translations.py:161 +msgid "Important" +msgstr "Важный" + +#. Translators: Issue severity +#: taiga/projects/translations.py:163 +msgid "Critical" +msgstr "Критический" + +#. Translators: User role +#: taiga/projects/translations.py:170 +msgid "UX" +msgstr "Юзабилити" + +#. Translators: User role +#: taiga/projects/translations.py:172 +msgid "Design" +msgstr "Дизайнер" + +#. Translators: User role +#: taiga/projects/translations.py:174 +msgid "Front" +msgstr "Фронтенд разработчик" + +#. Translators: User role +#: taiga/projects/translations.py:176 +msgid "Back" +msgstr "Бэкенд разработчик" + +#. Translators: User role +#: taiga/projects/translations.py:178 +msgid "Product Owner" +msgstr "Владелец продукта" + +#. Translators: User role +#: taiga/projects/translations.py:180 +msgid "Stakeholder" +msgstr "Заинтересованная сторона" + +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" +"У вас нет прав чтобы установить спринт для этой пользовательской истории." + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "" +"У вас нет прав чтобы установить статус для этой пользовательской истории." + +#: taiga/projects/userstories/api.py:254 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" +msgstr "" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "роль" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "порядок списка задач" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "порядок спринтов" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "дата окончания" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "является требованием клиента" + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "является требованием команды" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "создано из запроса" + +#: taiga/projects/userstories/validators.py:28 +msgid "There's no user story with that id" +msgstr "Не существует пользовательской истории с таким идентификатором" + +#: taiga/projects/validators.py:28 +msgid "There's no project with that id" +msgstr "Не существует проекта с таким идентификатором" + +#: taiga/projects/validators.py:37 +msgid "There's no user story status with that id" +msgstr "Не существует статуса пользовательской истории с таким идентификатором" + +#: taiga/projects/validators.py:46 +msgid "There's no task status with that id" +msgstr "Не существует статуса задачи с таким идентификатором" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:56 +msgid "Votes" +msgstr "Голоса" + +#: taiga/projects/votes/models.py:55 +msgid "Vote" +msgstr "Голосовать" + +#: taiga/projects/wiki/api.py:66 +msgid "'content' parameter is mandatory" +msgstr "параметр 'content' является обязательным" + +#: taiga/projects/wiki/api.py:69 +msgid "'project_id' parameter is mandatory" +msgstr "параметр 'project_id' является обязательным" + +#: taiga/projects/wiki/models.py:37 +msgid "last modifier" +msgstr "последний отредактировавший" + +#: taiga/projects/wiki/models.py:70 +msgid "href" +msgstr "href" + +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "Свертесть с историей API для получения изменений" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "Личные данные" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "Права доступа" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "Важные даты" + +#: taiga/users/api.py:111 +msgid "Duplicated email" +msgstr "Этот email уже используется" + +#: taiga/users/api.py:113 +msgid "Not valid email" +msgstr "Невалидный email" + +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "Неверное имя пользователя или e-mail" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "Письмо успешно отправлено!" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "Неверный токен" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "Поле \"текущий пароль\" является обязательным" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "Поле \"новый пароль\" является обязательным" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Неверная длина пароля, требуется как минимум 6 символов" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "Неверно указан текущий пароль" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "Список аргументов неполон" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "Неправильный формат изображения" + +#: taiga/users/api.py:256 taiga/users/api.py:262 +msgid "" +"Invalid, are you sure the token is correct and you didn't use it before?" +msgstr "Неверно, вы уверены что токен правильный и не использовался ранее?" + +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 +msgid "Invalid, are you sure the token is correct?" +msgstr "Неверно, вы уверены что токен правильный?" + +#: taiga/users/models.py:71 +msgid "superuser status" +msgstr "статус суперпользователя" + +#: taiga/users/models.py:72 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "Выбранный пользователь имеет все разрешения, ему не чего назначит." + +#: taiga/users/models.py:102 +msgid "username" +msgstr "имя пользователя" + +#: taiga/users/models.py:103 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" +msgstr "Обязательно. 30 символов или меньше. Буквы, числа и символы /./-/_" + +#: taiga/users/models.py:106 +msgid "Enter a valid username." +msgstr "Введите корректное имя пользователя." + +#: taiga/users/models.py:109 +msgid "active" +msgstr "активный" + +#: taiga/users/models.py:110 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "Выбранный пользователь активен. Отменить выбор для удаления аккаунта." + +#: taiga/users/models.py:116 +msgid "biography" +msgstr "биография" + +#: taiga/users/models.py:119 +msgid "photo" +msgstr "фотография" + +#: taiga/users/models.py:120 +msgid "date joined" +msgstr "когда присоединился" + +#: taiga/users/models.py:122 +msgid "default language" +msgstr "язык по умолчанию" + +#: taiga/users/models.py:124 +msgid "default theme" +msgstr "тема по умолчанию" + +#: taiga/users/models.py:126 +msgid "default timezone" +msgstr "временная зона по умолчанию" + +#: taiga/users/models.py:128 +msgid "colorize tags" +msgstr "установить цвета для тэгов" + +#: taiga/users/models.py:133 +msgid "email token" +msgstr "email токен" + +#: taiga/users/models.py:135 +msgid "new email address" +msgstr "новый email адрес" + +#: taiga/users/models.py:203 +msgid "permissions" +msgstr "разрешения" + +#: taiga/users/serializers.py:62 +msgid "invalid" +msgstr "невалидный" + +#: taiga/users/serializers.py:73 +msgid "Invalid username. Try with a different one." +msgstr "Неверное имя пользователя. Попробуйте другое." + +#: taiga/users/services.py:53 taiga/users/services.py:57 +msgid "Username or password does not matches user." +msgstr "Имя пользователя или пароль не соответствуют пользователю." + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Смена email

\n" +"

Здравствуйте, %(full_name)s,
подтвердите, пожалуйста, свой " +"email

\n" +" Подтвердить email\n" +"

Если вы не запрашивали смену email, не обращайте внимания на это " +"письмо.

\n" +"

Команда Taiga

\n" +" " + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Здравствуйте, %(full_name)s, подтвердите, пожалуйста, свой email\n" +"\n" +"%(url)s\n" +"\n" +"Вы можете проигнорировать это сообщение, если не указывали этот email.\n" +"\n" +"---\n" +"The Taiga Team\n" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "[Taiga] Изменить e-mail" + +#: taiga/users/templates/emails/password_recovery-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Recover your password

\n" +"

Hello %(full_name)s,
you asked to recover your password

\n" +" Recover your password\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

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

Восстановление пароля

\n" +"

Здравствуйте, %(full_name)s,
вы запросили восстановление " +"пароля

\n" +" Восстановить пароль\n" +"

Вы можете проигнорировать это сообщение, если не запрашивали " +"восстановление пароля.

\n" +"

Команда Taiga

\n" +" " + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"Здравствуйте, %(full_name)s, вы запросили восстановление пароля\n" +"\n" +"%(url)s\n" +"\n" +"Вы можете проигнорировать это сообщение, если не запрашивали восстановление " +"пароля.\n" +"\n" +"---\n" +"Команда Taiga\n" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "[Taiga] Восстановление пароля" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" \n" +" " +msgstr "" +"\n" +" \n" +"

Благодарим вас за регистрацию в Taiga

\n" +"

Мы надеемся, что вам понравится ей пользоваться

\n" +"

Мы сделали Taiga, потому что нам хотелось, чтобы программа " +"для управления проектами, которая весь день открыта у нас на компьютерах, " +"постоянно напоминала нам, почему нам нравиться работать вместе, " +"проектировать и программировать.

\n" +"

Мы сделали её красивой, элегантной, простой в использовании и " +"приятной - не жертвуя при этом гибкостью и возможностями.

\n" +" Команда Taiga\n" +" " + +#: taiga/users/templates/emails/registered_user-body-html.jinja:23 +#, python-format +msgid "" +"\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" +"\n" +" Вы можете удалить свой аккаунт посредством клика сюда\n" +" " + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" +"\n" +"Благодарим вас за регистрацию в Taiga\n" +"\n" +"Мы надеемся, что вам понравится ей пользоваться\n" +"\n" +"Мы сделали Taiga, потому что нам хотелось, чтобы программа для управления " +"проектами, которая весь день открыта у нас на компьютерах, постоянно " +"напоминала нам, почему нам нравиться работать вместе, проектировать и " +"программировать.\n" +"\n" +"Мы сделали её красивой, элегантной, простой в использовании и приятной - не " +"жертвуя при этом гибкостью и возможностями.\n" +"\n" +"--\n" +"Команда Taiga\n" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" +"\n" +"Вы можете удалить свой аккаунт из этого сервиса: %(url)s\n" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "Вы в Тайге!" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "Не существует роли с таким идентификатором" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" +"Дублирующий ключ, значение должно быть уникальны. Ключ '{}' уже существует." + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "ключ" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "URL" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "Секретный ключ" + +#: taiga/webhooks/models.py:39 +msgid "status code" +msgstr "код статуса" + +#: taiga/webhooks/models.py:40 +msgid "request data" +msgstr "данные запроса" + +#: taiga/webhooks/models.py:41 +msgid "request headers" +msgstr "заголовки запроса" + +#: taiga/webhooks/models.py:42 +msgid "response data" +msgstr "данные ответа" + +#: taiga/webhooks/models.py:43 +msgid "response headers" +msgstr "заголовки ответа" + +#: taiga/webhooks/models.py:44 +msgid "duration" +msgstr "длительность" diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po index 7eae5e43..240baf91 100644 --- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po +++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po @@ -1,5 +1,5 @@ # taiga-back.taiga. -# Copyright (C) 2015 Taiga Dev Team +# Copyright (C) 2014-2015 Taiga Dev Team # This file is distributed under the same license as the taiga-back package. # # Translators: @@ -11,10 +11,10 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-06-15 12:34+0200\n" -"PO-Revision-Date: 2015-06-27 02:13+0000\n" -"Last-Translator: Chi-Hsun Tsai \n" -"Language-Team: Chinese Traditional (http://www.transifex.com/projects/p/" +"POT-Creation-Date: 2015-11-02 09:24+0100\n" +"PO-Revision-Date: 2015-11-02 08:25+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/zh-Hant/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -34,40 +34,41 @@ msgstr "無效的註冊類型" msgid "invalid login type" msgstr "無效的登入類型" -#: taiga/auth/serializers.py:34 taiga/users/serializers.py:58 +#: taiga/auth/serializers.py:34 taiga/users/serializers.py:61 msgid "invalid username" msgstr "無效使用者名稱" -#: taiga/auth/serializers.py:39 taiga/users/serializers.py:64 +#: taiga/auth/serializers.py:39 taiga/users/serializers.py:67 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "必填。最多255字元(可為數字,字母,符號....)" -#: taiga/auth/services.py:75 +#: taiga/auth/services.py:73 msgid "Username is already in use." msgstr "本用戶名稱已被註冊" -#: taiga/auth/services.py:78 +#: taiga/auth/services.py:76 msgid "Email is already in use." msgstr "本電子郵件已使用" -#: taiga/auth/services.py:94 +#: taiga/auth/services.py:92 msgid "Token not matches any valid invitation." msgstr "代碼與任何有效的邀請不相符" -#: taiga/auth/services.py:122 +#: taiga/auth/services.py:120 msgid "User is already registered." msgstr "使用者已被註冊。" -#: taiga/auth/services.py:146 +#: taiga/auth/services.py:144 msgid "Membership with user is already exists." msgstr "使用者的成員資格已存在。" -#: taiga/auth/services.py:172 +#: taiga/auth/services.py:170 msgid "Error on creating new user." msgstr "無法創建新使用者" #: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +#: taiga/external_apps/services.py:34 msgid "Invalid token" msgstr "無效的代碼 " @@ -327,12 +328,12 @@ msgstr "因錯誤或無效參數,一致性出錯" msgid "Precondition error" msgstr "前提出錯" -#: taiga/base/filters.py:74 +#: taiga/base/filters.py:80 msgid "Error in filter params types." msgstr "過濾參數類型出錯" -#: taiga/base/filters.py:121 taiga/base/filters.py:210 -#: taiga/base/filters.py:259 +#: taiga/base/filters.py:134 taiga/base/filters.py:223 +#: taiga/base/filters.py:272 msgid "'project' must be an integer value." msgstr "專案須為整數值" @@ -551,32 +552,32 @@ msgstr "滙入標籤出錯" msgid "error importing timelines" msgstr "滙入時間軸出錯" -#: taiga/export_import/serializers.py:161 +#: taiga/export_import/serializers.py:163 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" 無法在此專案中找到" -#: taiga/export_import/serializers.py:382 +#: taiga/export_import/serializers.py:428 #: taiga/projects/custom_attributes/serializers.py:103 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "無效內容。必須為 {\"key\": \"value\",...}" -#: taiga/export_import/serializers.py:397 +#: taiga/export_import/serializers.py:443 #: taiga/projects/custom_attributes/serializers.py:118 msgid "It contain invalid custom fields." msgstr "包括無效慣例欄位" -#: taiga/export_import/serializers.py:466 -#: taiga/projects/milestones/serializers.py:63 -#: taiga/projects/serializers.py:66 taiga/projects/serializers.py:92 -#: taiga/projects/serializers.py:122 taiga/projects/serializers.py:164 +#: taiga/export_import/serializers.py:513 +#: taiga/projects/milestones/serializers.py:64 taiga/projects/serializers.py:70 +#: taiga/projects/serializers.py:96 taiga/projects/serializers.py:127 +#: taiga/projects/serializers.py:170 msgid "Name duplicated for the project" msgstr "專案的名稱被複製了" -#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +#: taiga/export_import/tasks.py:53 taiga/export_import/tasks.py:54 msgid "Error generating project dump" msgstr "產生專案傾倒時出錯" -#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +#: taiga/export_import/tasks.py:85 taiga/export_import/tasks.py:86 msgid "Error loading project dump" msgstr "載入專案傾倒時出錯" @@ -812,11 +813,61 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] 您堆存的專案已滙入" -#: taiga/feedback/models.py:23 taiga/users/models.py:111 +#: taiga/external_apps/api.py:40 taiga/external_apps/api.py:66 +#: taiga/external_apps/api.py:73 +msgid "Authentication required" +msgstr "" + +#: taiga/external_apps/models.py:33 +#: taiga/projects/custom_attributes/models.py:34 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:134 +#: taiga/projects/models.py:394 taiga/projects/models.py:433 +#: taiga/projects/models.py:458 taiga/projects/models.py:495 +#: taiga/projects/models.py:518 taiga/projects/models.py:541 +#: taiga/projects/models.py:576 taiga/projects/models.py:599 +#: taiga/users/models.py:198 taiga/webhooks/models.py:27 +msgid "name" +msgstr "姓名" + +#: taiga/external_apps/models.py:35 +msgid "Icon url" +msgstr "" + +#: taiga/external_apps/models.py:36 +msgid "web" +msgstr "" + +#: taiga/external_apps/models.py:37 taiga/projects/attachments/models.py:74 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/history/templatetags/functions.py:23 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:138 +#: taiga/projects/models.py:603 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "描述" + +#: taiga/external_apps/models.py:39 +msgid "Next url" +msgstr "" + +#: taiga/external_apps/models.py:41 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:55 taiga/projects/likes/models.py:50 +#: taiga/projects/notifications/models.py:84 taiga/projects/votes/models.py:50 +msgid "user" +msgstr "" + +#: taiga/external_apps/models.py:59 +msgid "application" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:113 msgid "full name" msgstr "全名" -#: taiga/feedback/models.py:25 taiga/users/models.py:106 +#: taiga/feedback/models.py:25 taiga/users/models.py:108 msgid "email address" msgstr "電子郵件" @@ -824,12 +875,14 @@ msgstr "電子郵件" msgid "comment" msgstr "評論" -#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 -#: taiga/projects/custom_attributes/models.py:38 -#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:129 taiga/projects/models.py:561 +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:44 +#: taiga/projects/issues/models.py:53 taiga/projects/likes/models.py:52 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:140 +#: taiga/projects/models.py:605 taiga/projects/notifications/models.py:86 #: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 -#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +#: taiga/projects/votes/models.py:52 taiga/projects/wiki/models.py:39 +#: taiga/userstorage/models.py:27 msgid "created date" msgstr "創建日期" @@ -895,7 +948,8 @@ msgstr "" msgid "The payload is not a valid json" msgstr "載荷為無效json" -#: taiga/hooks/api.py:61 +#: taiga/hooks/api.py:61 taiga/projects/issues/api.py:140 +#: taiga/projects/tasks/api.py:84 taiga/projects/userstories/api.py:109 msgid "The project doesn't exist" msgstr "專案不存在" @@ -903,29 +957,81 @@ msgstr "專案不存在" msgid "Bad signature" msgstr "錯誤簽名" -#: taiga/hooks/bitbucket/api.py:40 -msgid "The payload is not a valid application/x-www-form-urlencoded" -msgstr "載荷為無效應用 /x-www-form-urlencoded" - -#: taiga/hooks/bitbucket/event_hooks.py:45 -msgid "The payload is not valid" -msgstr "載荷無效" - -#: taiga/hooks/bitbucket/event_hooks.py:81 -#: taiga/hooks/github/event_hooks.py:76 taiga/hooks/gitlab/event_hooks.py:74 +#: taiga/hooks/bitbucket/event_hooks.py:84 taiga/hooks/github/event_hooks.py:75 +#: taiga/hooks/gitlab/event_hooks.py:73 msgid "The referenced element doesn't exist" msgstr "參考元素不存在" -#: taiga/hooks/bitbucket/event_hooks.py:88 -#: taiga/hooks/github/event_hooks.py:83 taiga/hooks/gitlab/event_hooks.py:81 +#: taiga/hooks/bitbucket/event_hooks.py:91 taiga/hooks/github/event_hooks.py:82 +#: taiga/hooks/gitlab/event_hooks.py:80 msgid "The status doesn't exist" msgstr "狀態不存在" -#: taiga/hooks/bitbucket/event_hooks.py:94 +#: taiga/hooks/bitbucket/event_hooks.py:97 msgid "Status changed from BitBucket commit" msgstr "來自BitBucket 投入的狀態更新" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/bitbucket/event_hooks.py:126 +#: taiga/hooks/github/event_hooks.py:141 taiga/hooks/gitlab/event_hooks.py:113 +msgid "Invalid issue information" +msgstr "無效的問題資訊" + +#: taiga/hooks/bitbucket/event_hooks.py:142 +#, python-brace-format +msgid "" +"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" +msgstr "" +"來自[@{bitbucket_user_name}]({bitbucket_user_url} 的問題\"詳見 " +"@{bitbucket_user_name}'s BitBucket profile\") BitBucket.\n" +"源自BitBucket 問題: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\"):\n" +"\n" +"{description}" + +#: taiga/hooks/bitbucket/event_hooks.py:153 +msgid "Issue created from BitBucket." +msgstr "來自BitBucket的問題:" + +#: taiga/hooks/bitbucket/event_hooks.py:177 +#: taiga/hooks/github/event_hooks.py:177 taiga/hooks/github/event_hooks.py:192 +#: taiga/hooks/gitlab/event_hooks.py:152 +msgid "Invalid issue comment information" +msgstr "無效的議題評論資訊" + +#: taiga/hooks/bitbucket/event_hooks.py:185 +#, python-brace-format +msgid "" +"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +" [@{bitbucket_user_name}]({bitbucket_user_url}之評論 \"參見 " +"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" +"源自BitBucket 問題: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"'bb#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/bitbucket/event_hooks.py:196 +#, python-brace-format +msgid "" +"Comment From BitBucket:\n" +"\n" +"{message}" +msgstr "" +"來自BitBucket的評論:\n" +"\n" +"{message}" + +#: taiga/hooks/github/event_hooks.py:96 #, python-brace-format msgid "" "Status changed by [@{github_user_name}]({github_user_url} \"See " @@ -936,15 +1042,11 @@ msgstr "" "@{github_user_name}'s GitHub profile\") 來自GitHub之投入 [{commit_id}]" "({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 +#: taiga/hooks/github/event_hooks.py:107 msgid "Status changed from GitHub commit." msgstr "來自GitHub投入的狀態更新" -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "無效的問題資訊" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/github/event_hooks.py:157 #, python-brace-format msgid "" "Issue created by [@{github_user_name}]({github_user_url} \"See " @@ -959,15 +1061,11 @@ msgstr "" "[gh#{number} - {subject}]({github_url} ”跳至 'gh#{number} - {subject}'\") \n" "{description}" -#: taiga/hooks/github/event_hooks.py:169 +#: taiga/hooks/github/event_hooks.py:168 msgid "Issue created from GitHub." msgstr "自來GitHub 的問題 " -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -msgid "Invalid issue comment information" -msgstr "無效的議題評論資訊" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/github/event_hooks.py:200 #, python-brace-format msgid "" "Comment by [@{github_user_name}]({github_user_url} \"See " @@ -983,7 +1081,7 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/github/event_hooks.py:212 +#: taiga/hooks/github/event_hooks.py:211 #, python-brace-format msgid "" "Comment From GitHub:\n" @@ -994,21 +1092,49 @@ msgstr "" "\n" "{message}" -#: taiga/hooks/gitlab/event_hooks.py:87 +#: taiga/hooks/gitlab/event_hooks.py:86 msgid "Status changed from GitLab commit" msgstr "來自GitLab提供的狀態變更" -#: taiga/hooks/gitlab/event_hooks.py:129 +#: taiga/hooks/gitlab/event_hooks.py:128 msgid "Created from GitLab" msgstr "創建立GitLab" +#: taiga/hooks/gitlab/event_hooks.py:160 +#, python-brace-format +msgid "" +"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " +"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" +"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" +msgstr "" +" [@{gitlab_user_name}]({gitlab_user_url}之評論 \"參見 @{gitlab_user_name}'s " +"GitLab profile\") from GitLab.\n" +"源自 GitLab 問題: [gl#{number} - {subject}]({gitlab_url} \"Go to " +"'gl#{number} - {subject}'\")\n" +"\n" +"{message}" + +#: taiga/hooks/gitlab/event_hooks.py:171 +#, python-brace-format +msgid "" +"Comment From GitLab:\n" +"\n" +"{message}" +msgstr "" +"來自GitLab的評論:\n" +"\n" +"{message}" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/permissions.py:51 msgid "View project" msgstr "檢視專案" #: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/permissions.py:53 msgid "View milestones" msgstr "檢視里程碑" @@ -1016,240 +1142,232 @@ msgstr "檢視里程碑" msgid "View user stories" msgstr "檢視使用者故事" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:35 +#: taiga/permissions/permissions.py:63 msgid "View tasks" msgstr "檢視任務 " #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/permissions.py:68 msgid "View issues" msgstr "檢視問題 " -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:75 +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:73 msgid "View wiki pages" msgstr "檢視維基頁" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:80 +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:78 msgid "View wiki links" msgstr "檢視維基連結" -#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 -msgid "Vote issues" -msgstr "票選問題 " - -#: taiga/permissions/permissions.py:39 +#: taiga/permissions/permissions.py:38 msgid "Request membership" msgstr "要求加入會員" -#: taiga/permissions/permissions.py:40 +#: taiga/permissions/permissions.py:39 msgid "Add user story to project" msgstr "專案中新增使用者故事" -#: taiga/permissions/permissions.py:41 +#: taiga/permissions/permissions.py:40 msgid "Add comments to user stories" msgstr "使用者故事附加評論" -#: taiga/permissions/permissions.py:42 +#: taiga/permissions/permissions.py:41 msgid "Add comments to tasks" msgstr "任務附加評論" -#: taiga/permissions/permissions.py:43 +#: taiga/permissions/permissions.py:42 msgid "Add issues" msgstr "加入問題 " -#: taiga/permissions/permissions.py:44 +#: taiga/permissions/permissions.py:43 msgid "Add comments to issues" msgstr "問題加入評論" -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +#: taiga/permissions/permissions.py:44 taiga/permissions/permissions.py:74 msgid "Add wiki page" msgstr "新增維基頁" -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Modify wiki page" msgstr "修改維基頁" -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:79 msgid "Add wiki link" msgstr "新增維基連結" -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 msgid "Modify wiki link" msgstr "修改維基連結" -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/permissions.py:54 msgid "Add milestone" msgstr "加入里程碑" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/permissions.py:55 msgid "Modify milestone" msgstr "修改里程碑" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/permissions.py:56 msgid "Delete milestone" msgstr "刪除里程碑 " -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/permissions.py:58 msgid "View user story" msgstr "檢視使用者故事" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/permissions.py:59 msgid "Add user story" msgstr "新增使用者故事" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/permissions.py:60 msgid "Modify user story" msgstr "修改使用者故事" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/permissions.py:61 msgid "Delete user story" msgstr "刪除使用者故事" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/permissions.py:64 msgid "Add task" msgstr "新增任務 " -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/permissions.py:65 msgid "Modify task" msgstr "修改任務 " -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/permissions.py:66 msgid "Delete task" msgstr "刪除任務 " -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/permissions.py:69 msgid "Add issue" msgstr "新增問題 " -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/permissions.py:70 msgid "Modify issue" msgstr "修改問題" -#: taiga/permissions/permissions.py:73 +#: taiga/permissions/permissions.py:71 msgid "Delete issue" msgstr "刪除問題 " -#: taiga/permissions/permissions.py:78 +#: taiga/permissions/permissions.py:76 msgid "Delete wiki page" msgstr "刪除維基頁 " -#: taiga/permissions/permissions.py:83 +#: taiga/permissions/permissions.py:81 msgid "Delete wiki link" msgstr "刪除維基連結" -#: taiga/permissions/permissions.py:87 +#: taiga/permissions/permissions.py:85 msgid "Modify project" msgstr "修改專案" -#: taiga/permissions/permissions.py:88 +#: taiga/permissions/permissions.py:86 msgid "Add member" msgstr "新增成員" -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/permissions.py:87 msgid "Remove member" msgstr "移除成員" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/permissions.py:88 msgid "Delete project" msgstr "刪除專案" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/permissions.py:89 msgid "Admin project values" msgstr "管理員專案數值" -#: taiga/permissions/permissions.py:92 +#: taiga/permissions/permissions.py:90 msgid "Admin roles" msgstr "管理員角色" -#: taiga/projects/api.py:204 +#: taiga/projects/api.py:202 msgid "Not valid template name" msgstr "非有效樣板名稱 " -#: taiga/projects/api.py:207 +#: taiga/projects/api.py:205 msgid "Not valid template description" msgstr "無效樣板描述" -#: taiga/projects/api.py:469 taiga/projects/serializers.py:257 +#: taiga/projects/api.py:481 taiga/projects/serializers.py:264 msgid "At least one of the user must be an active admin" msgstr "至少需有一位使用者擔任管理員" -#: taiga/projects/api.py:499 +#: taiga/projects/api.py:511 msgid "You don't have permisions to see that." msgstr "您無觀看權限" #: taiga/projects/attachments/api.py:47 -msgid "Non partial updates not supported" -msgstr "不支援非部份更新" +msgid "Partial updates are not supported" +msgstr "" #: taiga/projects/attachments/api.py:62 msgid "Project ID not matches between object and project" msgstr "專案ID不符合物件與專案" -#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 -#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:134 -#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/projects/attachments/models.py:52 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:145 +#: taiga/projects/notifications/models.py:59 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:35 #: taiga/userstorage/models.py:25 msgid "owner" msgstr "所有者" -#: taiga/projects/attachments/models.py:56 -#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/attachments/models.py:54 +#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 -#: taiga/projects/models.py:338 taiga/projects/models.py:364 -#: taiga/projects/models.py:395 taiga/projects/models.py:424 -#: taiga/projects/models.py:457 taiga/projects/models.py:480 -#: taiga/projects/models.py:507 taiga/projects/models.py:538 -#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 -#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 -#: taiga/projects/wiki/models.py:66 taiga/users/models.py:196 +#: taiga/projects/models.py:382 taiga/projects/models.py:408 +#: taiga/projects/models.py:439 taiga/projects/models.py:468 +#: taiga/projects/models.py:501 taiga/projects/models.py:524 +#: taiga/projects/models.py:551 taiga/projects/models.py:582 +#: taiga/projects/notifications/models.py:71 +#: taiga/projects/notifications/models.py:88 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:29 +#: taiga/projects/wiki/models.py:67 taiga/users/models.py:211 msgid "project" msgstr "專案" -#: taiga/projects/attachments/models.py:58 +#: taiga/projects/attachments/models.py:56 msgid "content type" msgstr "內容類型" -#: taiga/projects/attachments/models.py:60 +#: taiga/projects/attachments/models.py:58 msgid "object id" msgstr "物件ID" -#: taiga/projects/attachments/models.py:66 -#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/attachments/models.py:64 +#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 -#: taiga/projects/models.py:132 taiga/projects/models.py:564 +#: taiga/projects/models.py:143 taiga/projects/models.py:608 #: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 -#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +#: taiga/projects/wiki/models.py:42 taiga/userstorage/models.py:29 msgid "modified date" msgstr "修改日期" -#: taiga/projects/attachments/models.py:71 +#: taiga/projects/attachments/models.py:69 msgid "attached file" msgstr "附加檔案" -#: taiga/projects/attachments/models.py:74 +#: taiga/projects/attachments/models.py:71 +msgid "sha1" +msgstr "" + +#: taiga/projects/attachments/models.py:73 msgid "is deprecated" msgstr "棄用" #: taiga/projects/attachments/models.py:75 -#: taiga/projects/custom_attributes/models.py:32 -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:61 taiga/projects/models.py:127 -#: taiga/projects/models.py:559 taiga/projects/tasks/models.py:60 -#: taiga/projects/userstories/models.py:90 -msgid "description" -msgstr "描述" - -#: taiga/projects/attachments/models.py:76 -#: taiga/projects/custom_attributes/models.py:33 -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:354 -#: taiga/projects/models.py:391 taiga/projects/models.py:418 -#: taiga/projects/models.py:453 taiga/projects/models.py:476 -#: taiga/projects/models.py:501 taiga/projects/models.py:534 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:191 +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:398 +#: taiga/projects/models.py:435 taiga/projects/models.py:462 +#: taiga/projects/models.py:497 taiga/projects/models.py:520 +#: taiga/projects/models.py:545 taiga/projects/models.py:578 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:206 msgid "order" msgstr "次序" @@ -1262,33 +1380,44 @@ msgid "Jitsi" msgstr "Jitsi" #: taiga/projects/choices.py:23 +msgid "Custom" +msgstr "自定" + +#: taiga/projects/choices.py:24 msgid "Talky" msgstr "Talky" -#: taiga/projects/custom_attributes/models.py:31 -#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:123 -#: taiga/projects/models.py:350 taiga/projects/models.py:389 -#: taiga/projects/models.py:414 taiga/projects/models.py:451 -#: taiga/projects/models.py:474 taiga/projects/models.py:497 -#: taiga/projects/models.py:532 taiga/projects/models.py:555 -#: taiga/users/models.py:183 taiga/webhooks/models.py:27 -msgid "name" -msgstr "姓名" +#: taiga/projects/custom_attributes/choices.py:25 +msgid "Text" +msgstr "單行文字" -#: taiga/projects/custom_attributes/models.py:81 +#: taiga/projects/custom_attributes/choices.py:26 +msgid "Multi-Line Text" +msgstr "多行列文字" + +#: taiga/projects/custom_attributes/choices.py:27 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "類型" + +#: taiga/projects/custom_attributes/models.py:87 msgid "values" msgstr "價值" -#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/custom_attributes/models.py:97 #: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" msgstr "使用者故事" -#: taiga/projects/custom_attributes/models.py:106 +#: taiga/projects/custom_attributes/models.py:112 msgid "task" msgstr "任務 " -#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/custom_attributes/models.py:127 msgid "issue" msgstr "問題 " @@ -1368,7 +1497,7 @@ msgstr "移除 " #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 -#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 +#: taiga/projects/services/stats.py:138 taiga/projects/services/stats.py:139 msgid "Unassigned" msgstr "無指定" @@ -1415,33 +1544,37 @@ msgstr "來自:" msgid "To:" msgstr "給:" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:32 +#: taiga/projects/history/templatetags/functions.py:24 +#: taiga/projects/wiki/models.py:33 msgid "content" msgstr "內容" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/mixins/blocked.py:31 msgid "blocked note" msgstr "封鎖筆記" -#: taiga/projects/issues/api.py:139 +#: taiga/projects/history/templatetags/functions.py:26 +msgid "sprint" +msgstr "衝刺任務" + +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this sprint to this issue." msgstr "您無權限設定此問題的衝刺任務" -#: taiga/projects/issues/api.py:143 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this status to this issue." msgstr "您無權限設定此問題的狀態" -#: taiga/projects/issues/api.py:147 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this severity to this issue." msgstr "您無權限設定此問題的嚴重性" -#: taiga/projects/issues/api.py:151 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this priority to this issue." msgstr "您無權限設定此問題的優先性" -#: taiga/projects/issues/api.py:155 +#: taiga/projects/issues/api.py:176 msgid "You don't have permissions to set this type to this issue." msgstr "您無權限設定此問題的類型" @@ -1463,10 +1596,6 @@ msgstr "嚴重性" msgid "priority" msgstr "優先性" -#: taiga/projects/issues/models.py:46 -msgid "type" -msgstr "類型" - #: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 #: taiga/projects/userstories/models.py:60 msgid "milestone" @@ -1491,10 +1620,23 @@ msgstr "指派給" msgid "external reference" msgstr "外部參考" -#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:125 -#: taiga/projects/models.py:352 taiga/projects/models.py:416 -#: taiga/projects/models.py:499 taiga/projects/models.py:557 -#: taiga/projects/wiki/models.py:30 taiga/users/models.py:185 +#: taiga/projects/likes/models.py:28 taiga/projects/votes/models.py:28 +msgid "count" +msgstr "" + +#: taiga/projects/likes/models.py:31 taiga/projects/likes/models.py:32 +#: taiga/projects/likes/models.py:56 +msgid "Likes" +msgstr "" + +#: taiga/projects/likes/models.py:55 +msgid "Like" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:136 +#: taiga/projects/models.py:396 taiga/projects/models.py:460 +#: taiga/projects/models.py:543 taiga/projects/models.py:601 +#: taiga/projects/wiki/models.py:31 taiga/users/models.py:200 msgid "slug" msgstr "代稱" @@ -1506,8 +1648,8 @@ msgstr "预計開始日期" msgid "estimated finish date" msgstr "預計完成日期" -#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:356 -#: taiga/projects/models.py:420 taiga/projects/models.py:503 +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:400 +#: taiga/projects/models.py:464 taiga/projects/models.py:547 msgid "is closed" msgstr "被關閉" @@ -1536,215 +1678,220 @@ msgstr "'{param}' 參數為必要" msgid "'project' parameter is mandatory" msgstr "'project'參數為必要" -#: taiga/projects/models.py:59 +#: taiga/projects/models.py:66 msgid "email" msgstr "電子郵件" -#: taiga/projects/models.py:61 +#: taiga/projects/models.py:68 msgid "create at" msgstr "創建於" -#: taiga/projects/models.py:63 taiga/users/models.py:128 +#: taiga/projects/models.py:70 taiga/users/models.py:130 msgid "token" msgstr "代號" -#: taiga/projects/models.py:69 +#: taiga/projects/models.py:76 msgid "invitation extra text" msgstr "額外文案邀請" -#: taiga/projects/models.py:72 +#: taiga/projects/models.py:79 msgid "user order" msgstr "使用者次序" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:89 msgid "The user is already member of the project" msgstr "使用者已是專案成員" -#: taiga/projects/models.py:93 +#: taiga/projects/models.py:104 msgid "default points" msgstr "預設點數" -#: taiga/projects/models.py:97 +#: taiga/projects/models.py:108 msgid "default US status" msgstr "預設使用者故事狀態" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:112 msgid "default task status" msgstr "預設任務狀態" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:115 msgid "default priority" msgstr "預設優先性" -#: taiga/projects/models.py:107 +#: taiga/projects/models.py:118 msgid "default severity" msgstr "預設嚴重性" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:122 msgid "default issue status" msgstr "預設問題狀態" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:126 msgid "default issue type" msgstr "預設議題類型" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:147 msgid "members" msgstr "成員" -#: taiga/projects/models.py:139 +#: taiga/projects/models.py:150 msgid "total of milestones" msgstr "全部里程碑" -#: taiga/projects/models.py:140 +#: taiga/projects/models.py:151 msgid "total story points" msgstr "全部故事點數" -#: taiga/projects/models.py:143 taiga/projects/models.py:570 +#: taiga/projects/models.py:154 taiga/projects/models.py:614 msgid "active backlog panel" msgstr "活躍的待辦任務優先表面板" -#: taiga/projects/models.py:145 taiga/projects/models.py:572 +#: taiga/projects/models.py:156 taiga/projects/models.py:616 msgid "active kanban panel" msgstr "活躍的看板式面板" -#: taiga/projects/models.py:147 taiga/projects/models.py:574 +#: taiga/projects/models.py:158 taiga/projects/models.py:618 msgid "active wiki panel" msgstr "活躍的維基面板" -#: taiga/projects/models.py:149 taiga/projects/models.py:576 +#: taiga/projects/models.py:160 taiga/projects/models.py:620 msgid "active issues panel" msgstr "活躍的問題面板" -#: taiga/projects/models.py:152 taiga/projects/models.py:579 +#: taiga/projects/models.py:163 taiga/projects/models.py:623 msgid "videoconference system" msgstr "視訊會議系統" -#: taiga/projects/models.py:154 taiga/projects/models.py:581 -msgid "videoconference room salt" -msgstr "視訊會議房" +#: taiga/projects/models.py:165 taiga/projects/models.py:625 +msgid "videoconference extra data" +msgstr "視訊會議額外資料" -#: taiga/projects/models.py:159 +#: taiga/projects/models.py:170 msgid "creation template" msgstr "創建模版" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:173 msgid "anonymous permissions" msgstr "匿名權限" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:177 msgid "user permissions" msgstr "使用者權限" -#: taiga/projects/models.py:169 +#: taiga/projects/models.py:180 msgid "is private" msgstr "私密" -#: taiga/projects/models.py:180 +#: taiga/projects/models.py:191 msgid "tags colors" msgstr "標籤顏色" -#: taiga/projects/models.py:339 +#: taiga/projects/models.py:383 msgid "modules config" msgstr "模組設定" -#: taiga/projects/models.py:358 +#: taiga/projects/models.py:402 msgid "is archived" msgstr "已歸檔" -#: taiga/projects/models.py:360 taiga/projects/models.py:422 -#: taiga/projects/models.py:455 taiga/projects/models.py:478 -#: taiga/projects/models.py:505 taiga/projects/models.py:536 -#: taiga/users/models.py:113 +#: taiga/projects/models.py:404 taiga/projects/models.py:466 +#: taiga/projects/models.py:499 taiga/projects/models.py:522 +#: taiga/projects/models.py:549 taiga/projects/models.py:580 +#: taiga/users/models.py:115 msgid "color" msgstr "顏色" -#: taiga/projects/models.py:362 +#: taiga/projects/models.py:406 msgid "work in progress limit" msgstr "工作進度限制" -#: taiga/projects/models.py:393 taiga/userstorage/models.py:31 +#: taiga/projects/models.py:437 taiga/userstorage/models.py:31 msgid "value" msgstr "價值" -#: taiga/projects/models.py:567 +#: taiga/projects/models.py:611 msgid "default owner's role" msgstr "預設所有者角色" -#: taiga/projects/models.py:583 +#: taiga/projects/models.py:627 msgid "default options" msgstr "預設選項" -#: taiga/projects/models.py:584 +#: taiga/projects/models.py:628 msgid "us statuses" msgstr "我們狀況" -#: taiga/projects/models.py:585 taiga/projects/userstories/models.py:40 +#: taiga/projects/models.py:629 taiga/projects/userstories/models.py:40 #: taiga/projects/userstories/models.py:72 msgid "points" msgstr "點數" -#: taiga/projects/models.py:586 +#: taiga/projects/models.py:630 msgid "task statuses" msgstr "任務狀況" -#: taiga/projects/models.py:587 +#: taiga/projects/models.py:631 msgid "issue statuses" msgstr "問題狀況" -#: taiga/projects/models.py:588 +#: taiga/projects/models.py:632 msgid "issue types" msgstr "問題類型" -#: taiga/projects/models.py:589 +#: taiga/projects/models.py:633 msgid "priorities" msgstr "優先性" -#: taiga/projects/models.py:590 +#: taiga/projects/models.py:634 msgid "severities" msgstr "嚴重性" -#: taiga/projects/models.py:591 +#: taiga/projects/models.py:635 msgid "roles" msgstr "角色" #: taiga/projects/notifications/choices.py:28 -msgid "Not watching" -msgstr "不觀看" +msgid "Involved" +msgstr "" #: taiga/projects/notifications/choices.py:29 -msgid "Watching" -msgstr "觀看中" +msgid "All" +msgstr "" #: taiga/projects/notifications/choices.py:30 -msgid "Ignoring" -msgstr "忽視" +msgid "None" +msgstr "" -#: taiga/projects/notifications/mixins.py:87 -msgid "watchers" -msgstr "觀看者" - -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "created date time" msgstr "創建日期時間" -#: taiga/projects/notifications/models.py:61 +#: taiga/projects/notifications/models.py:63 msgid "updated date time" msgstr "更新日期時間" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:65 msgid "history entries" msgstr "歷史輸入" -#: taiga/projects/notifications/models.py:66 +#: taiga/projects/notifications/models.py:68 msgid "notify users" msgstr "通知用戶" -#: taiga/projects/notifications/services.py:63 -#: taiga/projects/notifications/services.py:77 +#: taiga/projects/notifications/models.py:90 +#: taiga/projects/notifications/models.py:91 +msgid "Watched" +msgstr "" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "通知特定使用者與專案退出" +#: taiga/projects/notifications/services.py:427 +msgid "Invalid value for notify level" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2466,7 +2613,7 @@ msgstr "" "\n" "[%(project)s] 刪除維基頁 \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:44 +#: taiga/projects/notifications/validators.py:46 msgid "Watchers contains invalid users" msgstr "監督者包含無效使用者" @@ -2490,66 +2637,69 @@ msgstr "版本" msgid "You can't leave the project if there are no more owners" msgstr "如果專案無所有者,你將無法脫離該專案" -#: taiga/projects/serializers.py:233 +#: taiga/projects/serializers.py:240 msgid "Email address is already taken" msgstr "電子郵件已使用" -#: taiga/projects/serializers.py:245 +#: taiga/projects/serializers.py:252 msgid "Invalid role for the project" msgstr "專案無效的角色" -#: taiga/projects/serializers.py:340 -msgid "Total milestones must be major or equal to zero" -msgstr "Kanban" - -#: taiga/projects/serializers.py:402 +#: taiga/projects/serializers.py:397 msgid "Default options" msgstr "預設選項" -#: taiga/projects/serializers.py:403 +#: taiga/projects/serializers.py:398 msgid "User story's statuses" msgstr "使用者故事狀態" -#: taiga/projects/serializers.py:404 +#: taiga/projects/serializers.py:399 msgid "Points" msgstr "點數" -#: taiga/projects/serializers.py:405 +#: taiga/projects/serializers.py:400 msgid "Task's statuses" msgstr "任務狀態" -#: taiga/projects/serializers.py:406 +#: taiga/projects/serializers.py:401 msgid "Issue's statuses" msgstr "問題狀態" -#: taiga/projects/serializers.py:407 +#: taiga/projects/serializers.py:402 msgid "Issue's types" msgstr "問題類型" -#: taiga/projects/serializers.py:408 +#: taiga/projects/serializers.py:403 msgid "Priorities" msgstr "優先性" -#: taiga/projects/serializers.py:409 +#: taiga/projects/serializers.py:404 msgid "Severities" msgstr "嚴重性" -#: taiga/projects/serializers.py:410 +#: taiga/projects/serializers.py:405 msgid "Roles" msgstr "角色" -#: taiga/projects/services/stats.py:72 +#: taiga/projects/services/stats.py:85 msgid "Future sprint" msgstr "未來之衝刺" -#: taiga/projects/services/stats.py:89 +#: taiga/projects/services/stats.py:102 msgid "Project End" msgstr "專案結束" -#: taiga/projects/tasks/api.py:58 taiga/projects/tasks/api.py:61 -#: taiga/projects/tasks/api.py:64 taiga/projects/tasks/api.py:67 -msgid "You don't have permissions for add/modify this task." -msgstr "您無新增或更改此任務的權限" +#: taiga/projects/tasks/api.py:104 taiga/projects/tasks/api.py:113 +msgid "You don't have permissions to set this sprint to this task." +msgstr "無權限更動此任務下的衝刺任務" + +#: taiga/projects/tasks/api.py:107 +msgid "You don't have permissions to set this user story to this task." +msgstr "無權限更動此務下的使用者故事" + +#: taiga/projects/tasks/api.py:110 +msgid "You don't have permissions to set this status to this task." +msgstr "無權限更動此任務下的狀態" #: taiga/projects/tasks/models.py:56 msgid "us order" @@ -2949,13 +3099,18 @@ msgstr "產品所有人" msgid "Stakeholder" msgstr "利害關係人" -#: taiga/projects/userstories/api.py:174 +#: taiga/projects/userstories/api.py:156 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "無權限更動使用者故事的衝刺任務" + +#: taiga/projects/userstories/api.py:160 +msgid "You don't have permissions to set this status to this user story." +msgstr "無權限更動此使用者故事的狀態" + +#: taiga/projects/userstories/api.py:254 #, python-brace-format -msgid "" -"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " -"{subject}\")" +msgid "Generating the user story #{ref} - {subject}" msgstr "" -"産生使用者故事[US #{ref} - {subject}](:us:{ref} \"US #{ref} - {subject}\")" #: taiga/projects/userstories/models.py:37 msgid "role" @@ -3003,34 +3158,34 @@ msgid "There's no task status with that id" msgstr "該ID無相關任務狀況" #: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 -#: taiga/projects/votes/models.py:54 +#: taiga/projects/votes/models.py:56 msgid "Votes" msgstr "投票數" -#: taiga/projects/votes/models.py:50 -msgid "votes" -msgstr "投票數" - -#: taiga/projects/votes/models.py:53 +#: taiga/projects/votes/models.py:55 msgid "Vote" msgstr "投票 " -#: taiga/projects/wiki/api.py:60 +#: taiga/projects/wiki/api.py:66 msgid "'content' parameter is mandatory" msgstr "'content'參數為必要" -#: taiga/projects/wiki/api.py:63 +#: taiga/projects/wiki/api.py:69 msgid "'project_id' parameter is mandatory" msgstr "'project_id'參數為必要" -#: taiga/projects/wiki/models.py:36 +#: taiga/projects/wiki/models.py:37 msgid "last modifier" msgstr "上次更改" -#: taiga/projects/wiki/models.py:69 +#: taiga/projects/wiki/models.py:70 msgid "href" msgstr "href" +#: taiga/timeline/signals.py:67 +msgid "Check the history API for the exact diff" +msgstr "" + #: taiga/users/admin.py:50 msgid "Personal info" msgstr "個人資訊" @@ -3043,141 +3198,141 @@ msgstr "許可" msgid "Important dates" msgstr "重要日期" -#: taiga/users/api.py:124 taiga/users/api.py:131 -msgid "Invalid username or email" -msgstr "無效使用者或郵件" - -#: taiga/users/api.py:140 -msgid "Mail sended successful!" -msgstr "成功送出郵件" - -#: taiga/users/api.py:152 taiga/users/api.py:157 -msgid "Token is invalid" -msgstr "代號無效" - -#: taiga/users/api.py:178 -msgid "Current password parameter needed" -msgstr "需要目前密碼之參數" - -#: taiga/users/api.py:181 -msgid "New password parameter needed" -msgstr "需要新密碼參數" - -#: taiga/users/api.py:184 -msgid "Invalid password length at least 6 charaters needed" -msgstr "無效密碼長度,至少需6個字元" - -#: taiga/users/api.py:187 -msgid "Invalid current password" -msgstr "無效密碼" - -#: taiga/users/api.py:203 -msgid "Incomplete arguments" -msgstr "不完整參數" - -#: taiga/users/api.py:208 -msgid "Invalid image format" -msgstr "無效的圖片檔案" - -#: taiga/users/api.py:261 +#: taiga/users/api.py:111 msgid "Duplicated email" msgstr "複製電子郵件" -#: taiga/users/api.py:263 +#: taiga/users/api.py:113 msgid "Not valid email" msgstr "非有效電子郵性" -#: taiga/users/api.py:283 taiga/users/api.py:289 +#: taiga/users/api.py:146 taiga/users/api.py:153 +msgid "Invalid username or email" +msgstr "無效使用者或郵件" + +#: taiga/users/api.py:161 +msgid "Mail sended successful!" +msgstr "成功送出郵件" + +#: taiga/users/api.py:173 taiga/users/api.py:178 +msgid "Token is invalid" +msgstr "代號無效" + +#: taiga/users/api.py:199 +msgid "Current password parameter needed" +msgstr "需要目前密碼之參數" + +#: taiga/users/api.py:202 +msgid "New password parameter needed" +msgstr "需要新密碼參數" + +#: taiga/users/api.py:205 +msgid "Invalid password length at least 6 charaters needed" +msgstr "無效密碼長度,至少需6個字元" + +#: taiga/users/api.py:208 +msgid "Invalid current password" +msgstr "無效密碼" + +#: taiga/users/api.py:224 +msgid "Incomplete arguments" +msgstr "不完整參數" + +#: taiga/users/api.py:229 +msgid "Invalid image format" +msgstr "無效的圖片檔案" + +#: taiga/users/api.py:256 taiga/users/api.py:262 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "無效,請確認代號正確,之前是否曾使用過?" -#: taiga/users/api.py:316 taiga/users/api.py:324 taiga/users/api.py:327 +#: taiga/users/api.py:289 taiga/users/api.py:297 taiga/users/api.py:300 msgid "Invalid, are you sure the token is correct?" msgstr "無效,請確認代號是否正確?" -#: taiga/users/models.py:69 +#: taiga/users/models.py:71 msgid "superuser status" msgstr "超級使用者狀態 " -#: taiga/users/models.py:70 +#: taiga/users/models.py:72 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "無經明確分派,即賦予該使用者所有權限," -#: taiga/users/models.py:100 +#: taiga/users/models.py:102 msgid "username" msgstr "使用者名稱" -#: taiga/users/models.py:101 +#: taiga/users/models.py:103 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "必填。最多30字元(可為數字,字母,符號....)" -#: taiga/users/models.py:104 +#: taiga/users/models.py:106 msgid "Enter a valid username." msgstr "輸入有效的使用者名稱 " -#: taiga/users/models.py:107 +#: taiga/users/models.py:109 msgid "active" msgstr "活躍" -#: taiga/users/models.py:108 +#: taiga/users/models.py:110 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "賦予該使用者活躍角色,以不選擇取代刪除帳戶功能。" -#: taiga/users/models.py:114 +#: taiga/users/models.py:116 msgid "biography" msgstr "自傳" -#: taiga/users/models.py:117 +#: taiga/users/models.py:119 msgid "photo" msgstr "照片" -#: taiga/users/models.py:118 +#: taiga/users/models.py:120 msgid "date joined" msgstr "加入日期" -#: taiga/users/models.py:120 +#: taiga/users/models.py:122 msgid "default language" msgstr "預設語言 " -#: taiga/users/models.py:122 +#: taiga/users/models.py:124 msgid "default theme" msgstr "預設主題" -#: taiga/users/models.py:124 +#: taiga/users/models.py:126 msgid "default timezone" msgstr "預設時區" -#: taiga/users/models.py:126 +#: taiga/users/models.py:128 msgid "colorize tags" msgstr "顏色標籤" -#: taiga/users/models.py:131 +#: taiga/users/models.py:133 msgid "email token" msgstr "電子郵件符號 " -#: taiga/users/models.py:133 +#: taiga/users/models.py:135 msgid "new email address" msgstr "新電子郵件地址" -#: taiga/users/models.py:188 +#: taiga/users/models.py:203 msgid "permissions" msgstr "許可" -#: taiga/users/serializers.py:59 +#: taiga/users/serializers.py:62 msgid "invalid" msgstr "無效" -#: taiga/users/serializers.py:70 +#: taiga/users/serializers.py:73 msgid "Invalid username. Try with a different one." msgstr "無效使用者名稱,請重試其它名稱 " -#: taiga/users/services.py:48 taiga/users/services.py:52 +#: taiga/users/services.py:53 taiga/users/services.py:57 msgid "Username or password does not matches user." msgstr "用戶名稱與密碼不符" diff --git a/taiga/mdrender/__init__.py b/taiga/mdrender/__init__.py index 8cef3795..abec3c69 100644 --- a/taiga/mdrender/__init__.py +++ b/taiga/mdrender/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/mdrender/extensions/emojify.py b/taiga/mdrender/extensions/emojify.py index cdf986ef..020ba329 100644 --- a/taiga/mdrender/extensions/emojify.py +++ b/taiga/mdrender/extensions/emojify.py @@ -27,7 +27,8 @@ import re -from django.conf import settings +from django.templatetags.static import static + from markdown.extensions import Extension from markdown.preprocessors import Preprocessor @@ -35,8 +36,8 @@ from markdown.preprocessors import Preprocessor # Grab the emojis (+800) here: https://github.com/arvida/emoji-cheat-sheet.com # This **crazy long** list was generated by walking through the emojis.png -emojis_path = "{}://{}/static/img/emojis/".format(settings.SITES["api"]["scheme"], settings.SITES["api"]["domain"]) -emojis_set = { +EMOJIS_PATH = "img/emojis/" +EMOJIS_SET = { "+1", "-1", "100", "1234", "8ball", "a", "ab", "abc", "abcd", "accept", "aerial_tramway", "airplane", "alarm_clock", "alien", "ambulance", "anchor", "angel", "anger", "angry", "anguished", "ant", "apple", "aquarius", "aries", "arrows_clockwise", "arrows_counterclockwise", "arrow_backward", "arrow_double_down", @@ -168,11 +169,11 @@ class EmojifyPreprocessor(Preprocessor): def emojify(match): emoji = match.group(1) - if emoji not in emojis_set: + if emoji not in EMOJIS_SET: return match.group(0) - url = emojis_path + emoji + u'.png' - + path = "{}{}.png".format(EMOJIS_PATH, emoji) + url = static(path) return '![{emoji}]({url})'.format(emoji=emoji, url=url) for line in lines: diff --git a/taiga/mdrender/extensions/mentions.py b/taiga/mdrender/extensions/mentions.py index 0664bd94..83eeae20 100644 --- a/taiga/mdrender/extensions/mentions.py +++ b/taiga/mdrender/extensions/mentions.py @@ -32,7 +32,7 @@ from taiga.users.models import User class MentionsExtension(Extension): def extendMarkdown(self, md, md_globals): - MENTION_RE = r'(@)([a-z0-9.-\.]+)' + MENTION_RE = r'(@)([a-zA-Z0-9.-\.]+)' mentionsPattern = MentionsPattern(MENTION_RE) mentionsPattern.md = md md.inlinePatterns.add('mentions', mentionsPattern, '_end') diff --git a/taiga/mdrender/extensions/target_link.py b/taiga/mdrender/extensions/target_link.py index 992399ea..7754d94b 100644 --- a/taiga/mdrender/extensions/target_link.py +++ b/taiga/mdrender/extensions/target_link.py @@ -1,7 +1,7 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán -# Copyright (C) 2015 Alejandro Alonso +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 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 diff --git a/taiga/mdrender/extensions/wikilinks.py b/taiga/mdrender/extensions/wikilinks.py index 1fd703b3..9d106d82 100644 --- a/taiga/mdrender/extensions/wikilinks.py +++ b/taiga/mdrender/extensions/wikilinks.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/mdrender/service.py b/taiga/mdrender/service.py index 5b9207c8..326f4b2c 100644 --- a/taiga/mdrender/service.py +++ b/taiga/mdrender/service.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -61,7 +61,7 @@ bleach.ALLOWED_STYLES.append("background") bleach.ALLOWED_ATTRIBUTES["a"] = ["href", "title", "alt", "target"] bleach.ALLOWED_ATTRIBUTES["img"] = ["alt", "src"] -bleach.ALLOWED_ATTRIBUTES["*"] = ["class", "style"] +bleach.ALLOWED_ATTRIBUTES["*"] = ["class", "style", "id"] def _make_extensions_list(project=None): @@ -75,11 +75,11 @@ def _make_extensions_list(project=None): MentionsExtension(), TaigaReferencesExtension(project), TargetBlankLinkExtension(), - "extra", - "codehilite", - "sane_lists", - "toc", - "nl2br"] + "markdown.extensions.extra", + "markdown.extensions.codehilite", + "markdown.extensions.sane_lists", + "markdown.extensions.toc", + "markdown.extensions.nl2br"] import diff_match_patch diff --git a/taiga/mdrender/templatetags/functions.py b/taiga/mdrender/templatetags/functions.py index 7608f553..fc15c39f 100644 --- a/taiga/mdrender/templatetags/functions.py +++ b/taiga/mdrender/templatetags/functions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -18,10 +18,8 @@ from django_jinja import library from jinja2 import Markup from taiga.mdrender.service import render -register = library.Library() - -@register.global_function +@library.global_function def mdrender(project, text) -> str: if text: return Markup(render(project, text)) diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py index b2e516f4..7761abbf 100644 --- a/taiga/permissions/permissions.py +++ b/taiga/permissions/permissions.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -32,7 +32,6 @@ USER_PERMISSIONS = [ ('view_milestones', _('View milestones')), ('view_us', _('View user stories')), ('view_issues', _('View issues')), - ('vote_issues', _('Vote issues')), ('view_tasks', _('View tasks')), ('view_wiki_pages', _('View wiki pages')), ('view_wiki_links', _('View wiki links')), @@ -41,7 +40,7 @@ USER_PERMISSIONS = [ ('add_comments_to_us', _('Add comments to user stories')), ('add_comments_to_task', _('Add comments to tasks')), ('add_issue', _('Add issues')), - ('add_comments_issue', _('Add comments to issues')), + ('add_comments_to_issue', _('Add comments to issues')), ('add_wiki_page', _('Add wiki page')), ('modify_wiki_page', _('Modify wiki page')), ('add_wiki_link', _('Add wiki link')), @@ -67,7 +66,6 @@ MEMBERS_PERMISSIONS = [ ('delete_task', _('Delete task')), # Issue permissions ('view_issues', _('View issues')), - ('vote_issues', _('Vote issues')), ('add_issue', _('Add issue')), ('modify_issue', _('Modify issue')), ('delete_issue', _('Delete issue')), diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py index d9df5bd7..90ed3050 100644 --- a/taiga/permissions/service.py +++ b/taiga/permissions/service.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -15,11 +15,12 @@ # 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 Membership, Project from .permissions import OWNERS_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 @@ -30,7 +31,7 @@ def _get_user_project_membership(user, project): def _get_object_project(obj): project = None - + Project = apps.get_model("projects", "Project") if isinstance(obj, Project): project = obj elif obj and hasattr(obj, 'project'): diff --git a/taiga/projects/__init__.py b/taiga/projects/__init__.py index 48278e73..c8c59bf3 100644 --- a/taiga/projects/__init__.py +++ b/taiga/projects/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index f3eea353..0dee8cde 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,7 +17,10 @@ from django.contrib import admin from taiga.projects.milestones.admin import MilestoneInline +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline from taiga.users.admin import RoleInline + from . import models class MembershipAdmin(admin.ModelAdmin): @@ -35,7 +38,7 @@ class ProjectAdmin(admin.ModelAdmin): list_display = ["name", "owner", "created_date", "total_milestones", "total_story_points"] list_display_links = list_display - inlines = [RoleInline, MembershipInline, MilestoneInline] + inlines = [RoleInline, MembershipInline, MilestoneInline, WatchedInline, VoteInline] def get_object(self, *args, **kwargs): self.obj = super().get_object(*args, **kwargs) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 36955ff5..ceebb2f7 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -31,12 +31,20 @@ from taiga.base.api.utils import get_object_or_404 from taiga.base.utils.slug import slugify_uniquely from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.utils import ( + attach_project_total_watchers_attrs_to_queryset, + attach_project_is_watcher_to_queryset, + attach_notify_level_to_project_queryset) + from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.userstories.models import UserStory, RolePoints from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue +from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin from taiga.permissions import service as permissions_service from . import serializers @@ -44,15 +52,12 @@ from . import models from . import permissions from . import services -from .votes import serializers as votes_serializers -from .votes import services as votes_service -from .votes.utils import attach_votescount_to_queryset ###################################################### ## Project ###################################################### - -class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): +class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet): + queryset = models.Project.objects.all() serializer_class = serializers.ProjectDetailSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer list_serializer_class = serializers.ProjectSerializer @@ -61,6 +66,32 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): filter_fields = (('member', 'members'),) order_by_fields = ("memberships__user_order",) + def get_queryset(self): + qs = super().get_queryset() + qs = self.attach_likes_attrs_to_queryset(qs) + qs = attach_project_total_watchers_attrs_to_queryset(qs) + if self.request.user.is_authenticated(): + qs = attach_project_is_watcher_to_queryset(qs, self.request.user) + qs = attach_notify_level_to_project_queryset(qs, self.request.user) + + return qs + + @detail_route(methods=["POST"]) + def watch(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "watch", project) + notify_level = request.DATA.get("notify_level", NotifyLevel.all) + project.add_watcher(self.request.user, notify_level=notify_level) + return response.Ok() + + @detail_route(methods=["POST"]) + def unwatch(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "unwatch", project) + user = self.request.user + project.remove_watcher(user) + return response.Ok() + @list_route(methods=["POST"]) def bulk_update_order(self, request, **kwargs): if self.request.user.is_anonymous(): @@ -74,10 +105,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): services.update_projects_order_in_bulk(data, "user_order", request.user) return response.NoContent(data=None) - def get_queryset(self): - qs = models.Project.objects.all() - return attach_votescount_to_queryset(qs, as_field="stars_count") - def get_serializer_class(self): if self.action == "list": return self.list_serializer_class @@ -160,41 +187,12 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): self.check_permissions(request, "issues_stats", project) return response.Ok(services.get_stats_for_project_issues(project)) - @detail_route(methods=["GET"]) - def issue_filters_data(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "issues_filters_data", project) - return response.Ok(services.get_issues_filters_data(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 star(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "star", project) - votes_service.add_vote(project, user=request.user) - return response.Ok() - - @detail_route(methods=["POST"]) - def unstar(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "unstar", project) - votes_service.remove_vote(project, user=request.user) - return response.Ok() - - @detail_route(methods=["GET"]) - def fans(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "fans", project) - - voters = votes_service.get_voters(project) - voters_data = votes_serializers.VoterSerializer(voters, many=True) - return response.Ok(voters_data.data) - @detail_route(methods=["POST"]) def create_template(self, request, **kwargs): template_name = request.DATA.get('template_name', None) @@ -293,6 +291,14 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): return response.NoContent() +class ProjectFansViewSet(FansViewSetMixin, ModelListViewSet): + permission_classes = (permissions.ProjectFansPermission,) + resource_model = models.Project + + +class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.ProjectWatchersPermission,) + resource_model = models.Project ###################################################### ## Custom values for selectors diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py index 2652563c..07938210 100644 --- a/taiga/projects/apps.py +++ b/taiga/projects/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,12 +27,7 @@ def connect_memberships_signals(): sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete') - # On membership object is deleted, update watchers of all objects relation. - signals.post_delete.connect(handlers.update_watchers_on_membership_post_delete, - sender=apps.get_model("projects", "Membership"), - dispatch_uid='update_watchers_on_membership_post_delete') - - # On membership object is deleted, update watchers of all objects relation. + # On membership object is deleted, update notify policies of all objects relation. signals.post_save.connect(handlers.create_notify_policy, sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy') @@ -53,9 +48,20 @@ def connect_projects_signals(): dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects") +def connect_us_status_signals(): + 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") + + +def connect_task_status_signals(): + 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") + + def disconnect_memberships_signals(): signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete') - signals.post_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='update_watchers_on_membership_post_delete') signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy') @@ -64,6 +70,12 @@ def disconnect_projects_signals(): signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"), dispatch_uid="tags_normalization_projects") signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects") +def disconnect_us_status_signals(): + signals.post_save.disconnect(sender=apps.get_model("projects", "UserStoryStatus"), dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status") + +def disconnect_task_status_signals(): + signals.post_save.disconnect(sender=apps.get_model("projects", "TaskStatus"), dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status") + class ProjectsAppConfig(AppConfig): name = "taiga.projects" @@ -72,3 +84,5 @@ class ProjectsAppConfig(AppConfig): def ready(self): connect_memberships_signals() connect_projects_signals() + connect_us_status_signals() + connect_task_status_signals() diff --git a/taiga/projects/attachments/__init__.py b/taiga/projects/attachments/__init__.py index e69de29b..17882254 100644 --- a/taiga/projects/attachments/__init__.py +++ b/taiga/projects/attachments/__init__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +default_app_config = "taiga.projects.attachments.apps.AttachmentsAppConfig" diff --git a/taiga/projects/attachments/admin.py b/taiga/projects/attachments/admin.py index e8d78de6..c30b4f52 100644 --- a/taiga/projects/attachments/admin.py +++ b/taiga/projects/attachments/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 0d26a0a8..0a40f8b7 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -44,7 +44,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru def update(self, *args, **kwargs): partial = kwargs.get("partial", False) if not partial: - raise exc.NotSupported(_("Non partial updates not supported")) + raise exc.NotSupported(_("Partial updates are not supported")) return super().update(*args, **kwargs) def get_content_type(self): @@ -56,7 +56,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru obj.content_type = self.get_content_type() obj.owner = self.request.user obj.size = obj.attached_file.size - obj.name = path.basename(obj.attached_file.name).lower() + obj.name = path.basename(obj.attached_file.name) if obj.project_id != obj.content_object.project_id: raise exc.WrongArguments(_("Project ID not matches between object and project")) diff --git a/taiga/projects/attachments/apps.py b/taiga/projects/attachments/apps.py new file mode 100644 index 00000000..c52458b9 --- /dev/null +++ b/taiga/projects/attachments/apps.py @@ -0,0 +1,38 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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 easy_thumbnails.files import get_thumbnailer + +from django.apps import AppConfig +from django.apps import apps +from django_transactional_cleanup.signals import cleanup_post_delete + + +def thumbnail_delete(**kwargs): + thumbnailer = get_thumbnailer(kwargs["file"]) + thumbnailer.delete_thumbnails() + + +def connect_attachment_signals(): + cleanup_post_delete.connect(thumbnail_delete) + + +class AttachmentsAppConfig(AppConfig): + name = "taiga.projects.attachments" + verbose_name = "Attachments" + + def ready(self): + connect_attachment_signals() diff --git a/taiga/projects/attachments/management/commands/__init__.py b/taiga/projects/attachments/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/attachments/management/commands/generate_sha1.py b/taiga/projects/attachments/management/commands/generate_sha1.py new file mode 100644 index 00000000..8441d127 --- /dev/null +++ b/taiga/projects/attachments/management/commands/generate_sha1.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from taiga.projects.attachments.models import Attachment + +import logging +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + @transaction.atomic + def handle(self, *args, **options): + total = rest = Attachment.objects.all().count() + + for attachment in Attachment.objects.all().order_by("id"): + attachment.save() + + rest -= 1 + logger.debug("[{} / {} remaining] - Generate sha1 for attach {}".format(rest, total, attachment.id)) diff --git a/taiga/projects/attachments/migrations/0005_attachment_sha1.py b/taiga/projects/attachments/migrations/0005_attachment_sha1.py new file mode 100644 index 00000000..c67043e1 --- /dev/null +++ b/taiga/projects/attachments/migrations/0005_attachment_sha1.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('attachments', '0004_auto_20150508_1141'), + ] + + operations = [ + migrations.AddField( + model_name='attachment', + name='sha1', + field=models.CharField(default='', verbose_name='sha1', max_length=40, blank=True), + preserve_default=True, + ), + ] \ No newline at end of file diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index c95c0f6b..f5c089fa 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,16 +27,14 @@ from django.contrib.contenttypes import generic from django.utils import timezone from django.utils.encoding import force_bytes from django.utils.translation import ugettext_lazy as _ -from django.template.defaultfilters import slugify +from django.utils.text import get_valid_filename from taiga.base.utils.iterators import split_by_n def get_attachment_file_path(instance, filename): - basename = path.basename(filename).lower() - base, ext = path.splitext(basename) - base = slugify(unidecode(base)) - basename = "".join([base, ext]) + basename = path.basename(filename) + basename = get_valid_filename(basename) hs = hashlib.sha256() hs.update(force_bytes(timezone.now().isoformat())) @@ -70,6 +68,7 @@ class Attachment(models.Model): upload_to=get_attachment_file_path, verbose_name=_("attached file")) + sha1 = models.CharField(default="", max_length=40, verbose_name=_("sha1"), blank=True) is_deprecated = models.BooleanField(default=False, verbose_name=_("is deprecated")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) @@ -85,11 +84,30 @@ class Attachment(models.Model): ("view_attachment", "Can view attachment"), ) + def __init__(self, *args, **kwargs): + super(Attachment, self).__init__(*args, **kwargs) + self._orig_attached_file = self.attached_file + + def _generate_sha1(self, blocksize=65536): + hasher = hashlib.sha1() + while True: + buff = self.attached_file.file.read(blocksize) + if not buff: + break + hasher.update(buff) + self.sha1 = hasher.hexdigest() + def save(self, *args, **kwargs): if not self._importing or not self.modified_date: self.modified_date = timezone.now() - - return super().save(*args, **kwargs) + if self.attached_file: + if not self.sha1 or self.attached_file != self._orig_attached_file: + self._generate_sha1() + save = super().save(*args, **kwargs) + self._orig_attached_file = self.attached_file + if self.attached_file: + self.attached_file.file.close() + return save def __str__(self): return "Attachment: {}".format(self.id) diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py index f709c378..603d4f98 100644 --- a/taiga/projects/attachments/permissions.py +++ b/taiga/projects/attachments/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 878b0e3c..549acabd 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -34,8 +34,8 @@ class AttachmentSerializer(serializers.ModelSerializer): model = models.Attachment fields = ("id", "project", "owner", "name", "attached_file", "size", "url", "description", "is_deprecated", "created_date", "modified_date", - "object_id", "order") - read_only_fields = ("owner", "created_date", "modified_date") + "object_id", "order", "sha1") + read_only_fields = ("owner", "created_date", "modified_date", "sha1") def get_url(self, obj): - return obj.attached_file.url + return obj.attached_file.url \ No newline at end of file diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py index 9447898d..0e443847 100644 --- a/taiga/projects/choices.py +++ b/taiga/projects/choices.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -20,5 +20,6 @@ from django.utils.translation import ugettext_lazy as _ VIDEOCONFERENCES_CHOICES = ( ("appear-in", _("AppearIn")), ("jitsi", _("Jitsi")), + ("custom", _("Custom")), ("talky", _("Talky")), ) diff --git a/taiga/projects/custom_attributes/admin.py b/taiga/projects/custom_attributes/admin.py index 201a31f0..fe0e3c6b 100644 --- a/taiga/projects/custom_attributes/admin.py +++ b/taiga/projects/custom_attributes/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index c93bb790..bb490d4e 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/custom_attributes/choices.py b/taiga/projects/custom_attributes/choices.py new file mode 100644 index 00000000..9c3e8468 --- /dev/null +++ b/taiga/projects/custom_attributes/choices.py @@ -0,0 +1,28 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.utils.translation import ugettext_lazy as _ + + +TEXT_TYPE = "text" +MULTILINE_TYPE = "multiline" +DATE_TYPE = "date" + +TYPES_CHOICES = ( + (TEXT_TYPE, _("Text")), + (MULTILINE_TYPE, _("Multi-Line Text")), + (DATE_TYPE, _("Date")) +) diff --git a/taiga/projects/custom_attributes/migrations/0006_auto_20151014_1645.py b/taiga/projects/custom_attributes/migrations/0006_auto_20151014_1645.py new file mode 100644 index 00000000..85698a38 --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0006_auto_20151014_1645.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', '0005_auto_20150505_1639'), + ] + + operations = [ + migrations.AddField( + model_name='issuecustomattribute', + name='type', + field=models.CharField(default='text', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date')], verbose_name='type', max_length=16), + ), + migrations.AddField( + model_name='taskcustomattribute', + name='type', + field=models.CharField(default='text', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date')], verbose_name='type', max_length=16), + ), + migrations.AddField( + model_name='userstorycustomattribute', + name='type', + field=models.CharField(default='text', choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date')], verbose_name='type', max_length=16), + ), + ] diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 6f82244d..51c81db6 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -22,14 +22,20 @@ from django_pgjson.fields import JsonField from taiga.projects.occ.mixins import OCCModelMixin +from . import choices + ###################################################### # Custom Attribute Models ####################################################### + class AbstractCustomAttribute(models.Model): name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) + type = models.CharField(null=False, blank=False, max_length=16, + choices=choices.TYPES_CHOICES, default=choices.TEXT_TYPE, + verbose_name=_("type")) order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("order")) project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss", verbose_name=_("project")) diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py index 14307d1a..7a780a57 100644 --- a/taiga/projects/custom_attributes/permissions.py +++ b/taiga/projects/custom_attributes/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 71a5ff5f..4b82a189 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/custom_attributes/services.py b/taiga/projects/custom_attributes/services.py index 7cbea6c4..1e5795cb 100644 --- a/taiga/projects/custom_attributes/services.py +++ b/taiga/projects/custom_attributes/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/custom_attributes/signals.py b/taiga/projects/custom_attributes/signals.py index fa90bb10..1e0e96e3 100644 --- a/taiga/projects/custom_attributes/signals.py +++ b/taiga/projects/custom_attributes/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index f0c2ebbb..46d369a5 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -16,7 +16,7 @@ "created_date": "2014-04-22T14:48:43.596Z", "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}", "slug": "scrum", - "videoconferences_salt": "", + "videoconferences_extra_data": "", "issue_statuses": "[{\"color\": \"#8C2318\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#5E8C6A\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#88A65E\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#BFB35A\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#89BAB4\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#CC0000\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#666666\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]", "default_owner_role": "product-owner", "issue_types": "[{\"color\": \"#89BAB4\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#ba89a8\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#89a8ba\", \"order\": 3, \"name\": \"Enhancement\"}]", @@ -43,7 +43,7 @@ "created_date": "2014-04-22T14:50:19.738Z", "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}", "slug": "kanban", - "videoconferences_salt": "", + "videoconferences_extra_data": "", "issue_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#d3d7cf\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#75507b\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]", "default_owner_role": "product-owner", "issue_types": "[{\"color\": \"#cc0000\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Enhancement\"}]", diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 2d6c365b..9e7bef23 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/history/choices.py b/taiga/projects/history/choices.py index 0895ca8c..1c38af8f 100644 --- a/taiga/projects/history/choices.py +++ b/taiga/projects/history/choices.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index a591c666..a51d3011 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -88,11 +88,6 @@ def _common_users_values(diff): if "owner" in diff: users.update(diff["owner"]) - if "watchers" in diff: - for ids in diff["watchers"]: - if not ids: - continue - users.update(ids) if "assigned_to" in diff: users.update(diff["assigned_to"]) if users: @@ -288,7 +283,6 @@ def userstory_freezer(us) -> dict: "milestone": us.milestone_id, "client_requirement": us.client_requirement, "team_requirement": us.team_requirement, - "watchers": [x.id for x in us.watchers.all()], "attachments": extract_attachments(us), "tags": us.tags, "points": points, @@ -315,7 +309,6 @@ def issue_freezer(issue) -> dict: "description": issue.description, "description_html": mdrender(issue.project, issue.description), "assigned_to": issue.assigned_to_id, - "watchers": [x.pk for x in issue.watchers.all()], "attachments": extract_attachments(issue), "tags": issue.tags, "is_blocked": issue.is_blocked, @@ -337,7 +330,6 @@ def task_freezer(task) -> dict: "description": task.description, "description_html": mdrender(task.project, task.description), "assigned_to": task.assigned_to_id, - "watchers": [x.pk for x in task.watchers.all()], "attachments": extract_attachments(task), "taskboard_order": task.taskboard_order, "us_order": task.us_order, @@ -359,7 +351,6 @@ def wikipage_freezer(wiki) -> dict: "owner": wiki.owner_id, "content": wiki.content, "content_html": mdrender(wiki.project, wiki.content), - "watchers": [x.pk for x in wiki.watchers.all()], "attachments": extract_attachments(wiki), } diff --git a/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py b/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py index dafe32ed..a1789405 100644 --- a/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py +++ b/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py @@ -15,7 +15,6 @@ def set_current_values_of_blocked_note_and_is_blocked_to_the_last_snapshot(apps, model = get_model_from_key(history_entry.key) pk = get_pk_from_key(history_entry.key) try: - print("Fixing history_entry: ", history_entry.created_at) obj = model.objects.get(pk=pk) save = False if hasattr(obj, "is_blocked") and "is_blocked" not in history_entry.snapshot: @@ -30,7 +29,7 @@ def set_current_values_of_blocked_note_and_is_blocked_to_the_last_snapshot(apps, history_entry.save() except ObjectDoesNotExist as e: - print("Ignoring {}".format(history_entry.pk)) + pass class Migration(migrations.Migration): diff --git a/taiga/projects/history/mixins.py b/taiga/projects/history/mixins.py index 9e379f77..27fa7632 100644 --- a/taiga/projects/history/mixins.py +++ b/taiga/projects/history/mixins.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,7 +17,7 @@ import warnings from .services import take_snapshot - +from taiga.projects.notifications import services as notifications_services class HistoryResourceMixin(object): """ @@ -63,6 +63,8 @@ class HistoryResourceMixin(object): if sobj != obj and delete: delete = False + notifications_services.analize_object_for_watchers(obj, comment, user) + self.__last_history = take_snapshot(sobj, comment=comment, user=user, delete=delete) self.__object_saved = True diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 82d1667d..cea42ae4 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 @@ -29,6 +29,9 @@ from .choices import HISTORY_TYPE_CHOICES from taiga.base.utils.diff import make_diff as make_diff_from_dicts +# This keys has been removed from freeze_impl so we can have objects where the +# previous diff has value for the attribute and we want to prevent their propagation +IGNORE_DIFF_FIELDS = [ "watchers", "description_diff", "content_diff", "blocked_note_diff"] def _generate_uuid(): return str(uuid.uuid1()) @@ -94,7 +97,10 @@ class HistoryEntry(models.Model): def owner(self): pk = self.user["pk"] model = apps.get_model("users", "User") - return model.objects.get(pk=pk) + try: + return model.objects.get(pk=pk) + except model.DoesNotExist: + return None @cached_property def values_diff(self): @@ -124,22 +130,12 @@ class HistoryEntry(models.Model): for key in self.diff: value = None - - # Note: Hack to prevent description_diff propagation - # on old HistoryEntry objects. - if key == "description_diff": - continue - elif key == "content_diff": - continue - elif key == "blocked_note_diff": + if key in IGNORE_DIFF_FIELDS: continue elif key in["description", "content", "blocked_note"]: (key, value) = resolve_diff_value(key) elif key in users_keys: value = [resolve_value("users", x) for x in self.diff[key]] - elif key == "watchers": - value = [[resolve_value("users", x) for x in self.diff[key][0]], - [resolve_value("users", x) for x in self.diff[key][1]]] elif key == "points": points = {} diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index c0adf8d6..636fe6a9 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index 4834e504..eab06fa7 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,17 +17,35 @@ from taiga.base.api import serializers from taiga.base.fields import JsonField, I18NJsonField +from taiga.users.services import get_photo_or_gravatar_url + from . import models + HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type") + class HistoryEntrySerializer(serializers.ModelSerializer): diff = JsonField() snapshot = JsonField() values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) - user = JsonField() + user = serializers.SerializerMethodField("get_user") delete_comment_user = JsonField() class Meta: model = models.HistoryEntry + + def get_user(self, entry): + user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} + user.update(entry.user) + user["photo"] = get_photo_or_gravatar_url(entry.owner) + + if entry.owner: + user["is_active"] = entry.owner.is_active + + if entry.owner.is_active or entry.owner.is_system: + user["name"] = entry.owner.get_full_name() + user["username"] = entry.owner.username + + return user diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 593eebad..a199f7ad 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014-2015 Andrey Antukh # 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 @@ -331,7 +331,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): "is_hidden": is_hidden, "is_snapshot": need_real_snapshot, } - + return entry_model.objects.create(**kwargs) diff --git a/taiga/projects/history/templatetags/functions.py b/taiga/projects/history/templatetags/functions.py index eef5133b..b5118c3a 100644 --- a/taiga/projects/history/templatetags/functions.py +++ b/taiga/projects/history/templatetags/functions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -18,17 +18,16 @@ from django.utils.translation import ugettext_lazy as _ from django_jinja import library -register = library.Library() - EXTRA_FIELD_VERBOSE_NAMES = { "description_diff": _("description"), "content_diff": _("content"), - "blocked_note_diff": _("blocked note") + "blocked_note_diff": _("blocked note"), + "milestone": _("sprint"), } -@register.global_function +@library.global_function def verbose_name(obj_class, field_name): if field_name in EXTRA_FIELD_VERBOSE_NAMES: return EXTRA_FIELD_VERBOSE_NAMES[field_name] @@ -38,6 +37,7 @@ def verbose_name(obj_class, field_name): except Exception: return field_name -@register.global_function + +@library.global_function def lists_diff(list1, list2): return (list(set(list1) - set(list2))) diff --git a/taiga/projects/issues/__init__.py b/taiga/projects/issues/__init__.py index aff90c37..b6be563e 100644 --- a/taiga/projects/issues/__init__.py +++ b/taiga/projects/issues/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/issues/admin.py b/taiga/projects/issues/admin.py index 16da297e..7da02b5b 100644 --- a/taiga/projects/issues/admin.py +++ b/taiga/projects/issues/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,13 +17,16 @@ from django.contrib import admin from taiga.projects.attachments.admin import AttachmentInline +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline + from . import models class IssueAdmin(admin.ModelAdmin): list_display = ["project", "milestone", "ref", "subject",] list_display_links = ["ref", "subject",] - # inlines = [AttachmentInline] + inlines = [WatchedInline, VoteInline] def get_object(self, *args, **kwargs): self.obj = super().get_object(*args, **kwargs) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 23294cd4..1df9875e 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,7 +16,7 @@ from django.utils.translation import ugettext as _ from django.db.models import Q -from django.http import Http404, HttpResponse +from django.http import HttpResponse from taiga.base import filters from taiga.base import exceptions as exc @@ -27,97 +27,56 @@ from taiga.base.api.utils import get_object_or_404 from taiga.users.models import User -from taiga.projects.notifications.mixins import WatchedResourceMixin +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 -from taiga.projects.votes.utils import attach_votescount_to_queryset -from taiga.projects.votes import services as votes_service -from taiga.projects.votes import serializers as votes_serializers +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 from . import services from . import permissions from . import serializers -class IssuesFilter(filters.FilterBackend): - filter_fields = ("status", "severity", "priority", "owner", "assigned_to", "tags", "type") - _special_values_dict = { - 'true': True, - 'false': False, - 'null': None, - } - - def _prepare_filters_data(self, request): - def _transform_value(value): - try: - return int(value) - except: - if value in self._special_values_dict.keys(): - return self._special_values_dict[value] - raise exc.BadRequest() - - data = {} - for filtername in self.filter_fields: - if filtername not in request.QUERY_PARAMS: - continue - - raw_value = request.QUERY_PARAMS[filtername] - values = set([x.strip() for x in raw_value.split(",")]) - - if filtername != "tags": - values = map(_transform_value, values) - - data[filtername] = list(values) - return data - - def filter_queryset(self, request, queryset, view): - filterdata = self._prepare_filters_data(request) - - if "tags" in filterdata: - queryset = queryset.filter(tags__contains=filterdata["tags"]) - - for name, value in filter(lambda x: x[0] != "tags", filterdata.items()): - if None in value: - qs_in_kwargs = {"{0}__in".format(name): [v for v in value if v is not None]} - qs_isnull_kwargs = {"{0}__isnull".format(name): True} - queryset = queryset.filter(Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs)) - else: - qs_kwargs = {"{0}__in".format(name): value} - queryset = queryset.filter(**qs_kwargs) - - return queryset - - -class IssuesOrdering(filters.FilterBackend): - def filter_queryset(self, request, queryset, view): - order_by = request.QUERY_PARAMS.get('order_by', None) - - if order_by in ['owner', '-owner', 'assigned_to', '-assigned_to']: - return queryset.order_by( - '{}__full_name'.format(order_by) - ) - return queryset - - -class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): +class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, + ModelCrudViewSet): + queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) + filter_backends = (filters.CanViewIssuesFilterBackend, + filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.IssueTypesFilter, + filters.SeveritiesFilter, + filters.PrioritiesFilter, + filters.TagsFilter, + filters.WatchersFilter, + filters.QFilter, + filters.OrderByFilterMixin) + retrieve_exclude_filters = (filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.IssueTypesFilter, + filters.SeveritiesFilter, + filters.PrioritiesFilter, + filters.TagsFilter, + filters.WatchersFilter,) - filter_backends = (filters.CanViewIssuesFilterBackend, filters.QFilter, - IssuesFilter, IssuesOrdering,) - retrieve_exclude_filters = (IssuesFilter,) - - filter_fields = ("project", "status__is_closed", "watchers") + filter_fields = ("project", + "status__is_closed") order_by_fields = ("type", - "severity", "status", + "severity", "priority", "created_date", "modified_date", "owner", "assigned_to", - "subject") + "subject", + "total_voters") def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: @@ -128,15 +87,70 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, return serializers.IssueSerializer + def update(self, request, *args, **kwargs): + self.object = self.get_object_or_none() + project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: + try: + new_project = Project.objects.get(pk=project_id) + self.check_permissions(request, "destroy", self.object) + self.check_permissions(request, "create", new_project) + + sprint_id = request.DATA.get('milestone', None) + if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: + request.DATA['milestone'] = None + + status_id = request.DATA.get('status', None) + if status_id is not None: + try: + old_status = self.object.project.issue_statuses.get(pk=status_id) + new_status = new_project.issue_statuses.get(slug=old_status.slug) + request.DATA['status'] = new_status.id + except IssueStatus.DoesNotExist: + request.DATA['status'] = new_project.default_issue_status.id + + priority_id = request.DATA.get('priority', None) + if priority_id is not None: + try: + old_priority = self.object.project.priorities.get(pk=priority_id) + new_priority = new_project.priorities.get(name=old_priority.name) + request.DATA['priority'] = new_priority.id + except Priority.DoesNotExist: + request.DATA['priority'] = new_project.default_priority.id + + severity_id = request.DATA.get('severity', None) + if severity_id is not None: + try: + old_severity = self.object.project.severities.get(pk=severity_id) + new_severity = new_project.severities.get(name=old_severity.name) + request.DATA['severity'] = new_severity.id + except Severity.DoesNotExist: + request.DATA['severity'] = new_project.default_severity.id + + type_id = request.DATA.get('type', None) + if type_id is not None: + try: + old_type = self.object.project.issue_types.get(pk=type_id) + new_type = new_project.issue_types.get(name=old_type.name) + request.DATA['type'] = new_type.id + except IssueType.DoesNotExist: + request.DATA['type'] = new_project.default_issue_type.id + + except Project.DoesNotExist: + return response.BadRequest(_("The project doesn't exist")) + + return super().update(request, *args, **kwargs) + def get_queryset(self): - qs = models.Issue.objects.all() + qs = super().get_queryset() qs = qs.prefetch_related("attachments") - qs = attach_votescount_to_queryset(qs, as_field="votes_count") - return qs + qs = self.attach_votes_attrs_to_queryset(qs) + return self.attach_watchers_attrs_to_queryset(qs) def pre_save(self, obj): if not obj.id: obj.owner = self.request.user + super().pre_save(obj) def pre_conditions_on_save(self, obj): @@ -169,6 +183,32 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id) return self.retrieve(request, pk=issue.pk) + @list_route(methods=["GET"]) + def filters_data(self, request, *args, **kwargs): + project_id = request.QUERY_PARAMS.get("project", None) + project = get_object_or_404(Project, id=project_id) + + filter_backends = self.get_filter_backends() + types_filter_backends = (f for f in filter_backends if f != filters.IssueTypesFilter) + statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) + assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) + owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) + priorities_filter_backends = (f for f in filter_backends if f != filters.PrioritiesFilter) + severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter) + tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter) + + queryset = self.get_queryset() + querysets = { + "types": self.filter_queryset(queryset, filter_backends=types_filter_backends), + "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), + "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), + "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), + "priorities": self.filter_queryset(queryset, filter_backends=priorities_filter_backends), + "severities": self.filter_queryset(queryset, filter_backends=severities_filter_backends), + "tags": self.filter_queryset(queryset) + } + return response.Ok(services.get_issues_filters_data(project, querysets)) + @list_route(methods=["GET"]) def csv(self, request): uuid = request.QUERY_PARAMS.get("uuid", None) @@ -200,51 +240,12 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, return response.BadRequest(serializer.errors) - @detail_route(methods=['post']) - def upvote(self, request, pk=None): - issue = get_object_or_404(models.Issue, pk=pk) - self.check_permissions(request, 'upvote', issue) - - votes_service.add_vote(issue, user=request.user) - return response.Ok() - - @detail_route(methods=['post']) - def downvote(self, request, pk=None): - issue = get_object_or_404(models.Issue, pk=pk) - - self.check_permissions(request, 'downvote', issue) - - votes_service.remove_vote(issue, user=request.user) - return response.Ok() +class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.IssueVotersPermission,) + resource_model = models.Issue -class VotersViewSet(ModelListViewSet): - serializer_class = votes_serializers.VoterSerializer - list_serializer_class = votes_serializers.VoterSerializer - permission_classes = (permissions.IssueVotersPermission, ) - - def retrieve(self, request, *args, **kwargs): - pk = kwargs.get("pk", None) - issue_id = kwargs.get("issue_id", None) - issue = get_object_or_404(models.Issue, pk=issue_id) - - self.check_permissions(request, 'retrieve', issue) - - try: - self.object = votes_service.get_voters(issue).get(pk=pk) - except User.DoesNotExist: - raise Http404 - - serializer = self.get_serializer(self.object) - return response.Ok(serializer.data) - - def list(self, request, *args, **kwargs): - issue_id = kwargs.get("issue_id", None) - issue = get_object_or_404(models.Issue, pk=issue_id) - self.check_permissions(request, 'list', issue) - return super().list(request, *args, **kwargs) - - def get_queryset(self): - issue = models.Issue.objects.get(pk=self.kwargs.get("issue_id")) - return votes_service.get_voters(issue) +class IssueWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.IssueWatchersPermission,) + resource_model = models.Issue diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index cc68fb73..485513f0 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/issues/migrations/0003_auto_20141210_1108.py b/taiga/projects/issues/migrations/0003_auto_20141210_1108.py index b8ee567c..ebc22056 100644 --- a/taiga/projects/issues/migrations/0003_auto_20141210_1108.py +++ b/taiga/projects/issues/migrations/0003_auto_20141210_1108.py @@ -21,7 +21,6 @@ def _fix_tags_model(tags_model): def fix_tags(apps, schema_editor): - print("Fixing user issue tags") _fix_tags_model(Issue) diff --git a/taiga/projects/issues/migrations/0006_remove_issue_watchers.py b/taiga/projects/issues/migrations/0006_remove_issue_watchers.py new file mode 100644 index 00000000..c612acb1 --- /dev/null +++ b/taiga/projects/issues/migrations/0006_remove_issue_watchers.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import connection +from django.db import models, migrations +from django.contrib.contenttypes.models import ContentType +from taiga.base.utils.contenttypes import update_all_contenttypes + +def create_notifications(apps, schema_editor): + update_all_contenttypes(verbosity=0) + sql=""" +INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id) +SELECT issue_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id +FROM issues_issue_watchers INNER JOIN issues_issue ON issues_issue_watchers.issue_id = issues_issue.id""".format(content_type_id=ContentType.objects.get(model='issue').id) + cursor = connection.cursor() + cursor.execute(sql) + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_watched'), + ('issues', '0005_auto_20150623_1923'), + ] + + operations = [ + migrations.RunPython(create_notifications), + migrations.RemoveField( + model_name='issue', + name='watchers', + ), + ] diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 943509b3..81745d4a 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index eeeebdb0..8c8ddd4d 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -29,11 +29,14 @@ class IssuePermission(TaigaResourcePermission): partial_update_perms = HasProjectPerm('modify_issue') destroy_perms = HasProjectPerm('delete_issue') list_perms = AllowAny() + filters_data_perms = AllowAny() csv_perms = AllowAny() - upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') - downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') bulk_create_perms = HasProjectPerm('add_issue') delete_comment_perms= HasProjectPerm('modify_issue') + upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues') + downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues') + watch_perms = IsAuthenticated() & HasProjectPerm('view_issues') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues') class HasIssueIdUrlParam(PermissionComponent): @@ -48,8 +51,11 @@ class IssueVotersPermission(TaigaResourcePermission): enought_perms = IsProjectOwner() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_issues') - create_perms = HasProjectPerm('add_issue') - update_perms = HasProjectPerm('modify_issue') - partial_update_perms = HasProjectPerm('modify_issue') - destroy_perms = HasProjectPerm('delete_issue') + list_perms = HasProjectPerm('view_issues') + + +class IssueWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_issues') list_perms = HasProjectPerm('view_issues') diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 2cd60104..f4457a5f 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -19,17 +19,19 @@ from taiga.base.fields import TagsField from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin - from taiga.mdrender.service import render as mdrender from taiga.projects.validators import ProjectExistsValidator from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicIssueStatusSerializer -from taiga.users.serializers import BasicInfoSerializer as UserBasicInfoSerializer +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin + +from taiga.users.serializers import UserBasicInfoSerializer from . import models -class IssueSerializer(WatchersValidator, serializers.ModelSerializer): +class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): tags = TagsField(required=False) external_reference = PgArrayField(required=False) is_closed = serializers.Field(source="is_closed") @@ -37,9 +39,9 @@ class IssueSerializer(WatchersValidator, serializers.ModelSerializer): generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories") blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") description_html = serializers.SerializerMethodField("get_description_html") - votes = serializers.SerializerMethodField("get_votes_number") status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True) assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) + owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) class Meta: model = models.Issue @@ -58,9 +60,12 @@ class IssueSerializer(WatchersValidator, serializers.ModelSerializer): def get_description_html(self, obj): return mdrender(obj.project, obj.description) - def get_votes_number(self, obj): - # The "votes_count" attribute is attached in the get_queryset of the viewset. - return getattr(obj, "votes_count", 0) + +class IssueListSerializer(IssueSerializer): + class Meta: + model = models.Issue + read_only_fields = ('id', 'ref', 'created_date', 'modified_date') + exclude=("description", "description_html") class IssueListSerializer(IssueSerializer): diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index 42382578..2929aef1 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,12 +16,18 @@ import io import csv +from collections import OrderedDict +from operator import itemgetter +from contextlib import closing + +from django.db import connection +from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.issues.apps import ( connect_issues_signals, disconnect_issues_signals) - +from taiga.projects.votes import services as votes_services from . import models @@ -78,7 +84,8 @@ def issues_to_csv(project, queryset): fieldnames = ["ref", "subject", "description", "milestone", "owner", "owner_full_name", "assigned_to", "assigned_to_full_name", "status", "severity", "priority", "type", "is_closed", - "attachments", "external_reference", "tags"] + "attachments", "external_reference", "tags", + "watchers", "voters"] for custom_attr in project.issuecustomattributes.all(): fieldnames.append(custom_attr.name) @@ -102,6 +109,8 @@ def issues_to_csv(project, queryset): "attachments": issue.attachments.count(), "external_reference": issue.external_reference, "tags": ",".join(issue.tags or []), + "watchers": [u.id for u in issue.get_watchers()], + "voters": votes_services.get_voters(issue).count(), } for custom_attr in project.issuecustomattributes.all(): @@ -111,3 +120,258 @@ def issues_to_csv(project, queryset): writer.writerow(issue_data) return csv_data + + +def _get_issues_statuses(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_issuestatus"."id", + "projects_issuestatus"."name", + "projects_issuestatus"."color", + "projects_issuestatus"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."status_id" = "projects_issuestatus"."id") + FROM "projects_issuestatus" + WHERE "projects_issuestatus"."project_id" = %s + ORDER BY "projects_issuestatus"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_types(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_issuetype"."id", + "projects_issuetype"."name", + "projects_issuetype"."color", + "projects_issuetype"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."type_id" = "projects_issuetype"."id") + FROM "projects_issuetype" + WHERE "projects_issuetype"."project_id" = %s + ORDER BY "projects_issuetype"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_priorities(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_priority"."id", + "projects_priority"."name", + "projects_priority"."color", + "projects_priority"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."priority_id" = "projects_priority"."id") + FROM "projects_priority" + WHERE "projects_priority"."project_id" = %s + ORDER BY "projects_priority"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_severities(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_severity"."id", + "projects_severity"."name", + "projects_severity"."color", + "projects_severity"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."severity_id" = "projects_severity"."id") + FROM "projects_severity" + WHERE "projects_severity"."project_id" = %s + ORDER BY "projects_severity"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_assigned_to(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT NULL, + NULL, + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id" ) + WHERE {where} AND "issues_issue"."assigned_to_id" IS NULL) + UNION SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id" ) + WHERE {where} AND "issues_issue"."assigned_to_id" = "projects_membership"."user_id") + FROM "projects_membership" + INNER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + result.append({ + "id": id, + "full_name": full_name or "", + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_issues_owners(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} and "issues_issue"."owner_id" = "projects_membership"."user_id") + FROM "projects_membership" + RIGHT OUTER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL) + OR ("users_user"."is_system" IS TRUE); + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + if count > 0: + result.append({ + "id": id, + "full_name": full_name, + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_issues_tags(queryset): + tags = [] + for t_list in queryset.values_list("tags", flat=True): + if t_list is None: + continue + tags += list(t_list) + + tags = [{"name":e, "count":tags.count(e)} for e in set(tags)] + + return sorted(tags, key=itemgetter("name")) + + +def get_issues_filters_data(project, querysets): + """ + Given a project and an issues queryset, return a simple data structure + of all possible filters for the issues in the queryset. + """ + data = OrderedDict([ + ("types", _get_issues_types(project, querysets["types"])), + ("statuses", _get_issues_statuses(project, querysets["statuses"])), + ("priorities", _get_issues_priorities(project, querysets["priorities"])), + ("severities", _get_issues_severities(project, querysets["severities"])), + ("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])), + ("owners", _get_issues_owners(project, querysets["owners"])), + ("tags", _get_issues_tags(querysets["tags"])), + ]) + + return data diff --git a/taiga/projects/issues/signals.py b/taiga/projects/issues/signals.py index 9389d410..1a50a911 100644 --- a/taiga/projects/issues/signals.py +++ b/taiga/projects/issues/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/likes/__init__.py b/taiga/projects/likes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/likes/admin.py b/taiga/projects/likes/admin.py new file mode 100644 index 00000000..802eaca4 --- /dev/null +++ b/taiga/projects/likes/admin.py @@ -0,0 +1,25 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline + +from . import models + + +class LikeInline(GenericTabularInline): + model = models.Like + extra = 0 diff --git a/taiga/projects/likes/migrations/0001_initial.py b/taiga/projects/likes/migrations/0001_initial.py new file mode 100644 index 00000000..e1a9dd6d --- /dev/null +++ b/taiga/projects/likes/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Like', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('created_date', models.DateTimeField(verbose_name='created date', auto_now_add=True)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ('user', models.ForeignKey(related_name='likes', verbose_name='user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Like', + 'verbose_name_plural': 'Likes', + }, + ), + migrations.CreateModel( + name='Likes', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('count', models.PositiveIntegerField(default=0, verbose_name='count')), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Likes', + 'verbose_name_plural': 'Likes', + }, + ), + migrations.AlterUniqueTogether( + name='likes', + unique_together=set([('content_type', 'object_id')]), + ), + migrations.AlterUniqueTogether( + name='like', + unique_together=set([('content_type', 'object_id', 'user')]), + ), + ] diff --git a/taiga/projects/likes/migrations/__init__.py b/taiga/projects/likes/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/likes/mixins/__init__.py b/taiga/projects/likes/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/likes/mixins/serializers.py b/taiga/projects/likes/mixins/serializers.py new file mode 100644 index 00000000..a4875b86 --- /dev/null +++ b/taiga/projects/likes/mixins/serializers.py @@ -0,0 +1,30 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api import serializers + + +class FanResourceSerializerMixin(serializers.ModelSerializer): + is_fan = serializers.SerializerMethodField("get_is_fan") + total_fans = serializers.SerializerMethodField("get_total_fans") + + def get_is_fan(self, obj): + # The "is_fan" attribute is attached in the get_queryset of the viewset. + return getattr(obj, "is_fan", False) or False + + def get_total_fans(self, obj): + # The "total_fans" attribute is attached in the get_queryset of the viewset. + return getattr(obj, "total_fans", 0) or 0 diff --git a/taiga/projects/likes/mixins/viewsets.py b/taiga/projects/likes/mixins/viewsets.py new file mode 100644 index 00000000..b3d9b2e1 --- /dev/null +++ b/taiga/projects/likes/mixins/viewsets.py @@ -0,0 +1,92 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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.exceptions import ObjectDoesNotExist + +from taiga.base import response +from taiga.base.api import viewsets +from taiga.base.api.utils import get_object_or_404 +from taiga.base.decorators import detail_route + +from taiga.projects.likes import serializers +from taiga.projects.likes import services +from taiga.projects.likes.utils import attach_total_fans_to_queryset, attach_is_fan_to_queryset + + +class LikedResourceMixin: + # Note: Update get_queryset method: + # def get_queryset(self): + # qs = super().get_queryset() + # return self.attach_likes_attrs_to_queryset(qs) + + def attach_likes_attrs_to_queryset(self, queryset): + qs = attach_total_fans_to_queryset(queryset) + + if self.request.user.is_authenticated(): + qs = attach_is_fan_to_queryset(self.request.user, qs) + + return qs + + @detail_route(methods=["POST"]) + def like(self, request, pk=None): + obj = self.get_object() + self.check_permissions(request, "like", obj) + + services.add_like(obj, user=request.user) + return response.Ok() + + @detail_route(methods=["POST"]) + def unlike(self, request, pk=None): + obj = self.get_object() + self.check_permissions(request, "unlike", obj) + + services.remove_like(obj, user=request.user) + return response.Ok() + + +class FansViewSetMixin: + # Is a ModelListViewSet with two required params: permission_classes and resource_model + serializer_class = serializers.FanSerializer + list_serializer_class = serializers.FanSerializer + permission_classes = None + resource_model = None + + def retrieve(self, request, *args, **kwargs): + pk = kwargs.get("pk", None) + resource_id = kwargs.get("resource_id", None) + resource = get_object_or_404(self.resource_model, pk=resource_id) + + self.check_permissions(request, 'retrieve', resource) + + try: + self.object = services.get_fans(resource).get(pk=pk) + except ObjectDoesNotExist: # or User.DoesNotExist + return response.NotFound() + + serializer = self.get_serializer(self.object) + return response.Ok(serializer.data) + + def list(self, request, *args, **kwargs): + resource_id = kwargs.get("resource_id", None) + resource = get_object_or_404(self.resource_model, pk=resource_id) + + self.check_permissions(request, 'list', resource) + + return super().list(request, *args, **kwargs) + + def get_queryset(self): + resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id")) + return services.get_fans(resource) diff --git a/taiga/projects/likes/models.py b/taiga/projects/likes/models.py new file mode 100644 index 00000000..9b56f923 --- /dev/null +++ b/taiga/projects/likes/models.py @@ -0,0 +1,66 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings +from django.contrib.contenttypes import generic +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class Likes(models.Model): + content_type = models.ForeignKey("contenttypes.ContentType") + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey("content_type", "object_id") + count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count")) + + class Meta: + verbose_name = _("Likes") + verbose_name_plural = _("Likes") + unique_together = ("content_type", "object_id") + + @property + def project(self): + if hasattr(self.content_object, 'project'): + return self.content_object.project + return None + + def __str__(self): + return self.count + + +class Like(models.Model): + content_type = models.ForeignKey("contenttypes.ContentType") + object_id = models.PositiveIntegerField() + content_object = generic.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, + verbose_name=_("created date")) + + class Meta: + verbose_name = _("Like") + verbose_name_plural = _("Likes") + unique_together = ("content_type", "object_id", "user") + + @property + def project(self): + if hasattr(self.content_object, 'project'): + return self.content_object.project + return None + + def __str__(self): + return self.user.get_full_name() diff --git a/taiga/projects/likes/serializers.py b/taiga/projects/likes/serializers.py new file mode 100644 index 00000000..c507166e --- /dev/null +++ b/taiga/projects/likes/serializers.py @@ -0,0 +1,30 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api import serializers +from taiga.base.fields import TagsField + +from taiga.users.models import User +from taiga.users.services import get_photo_or_gravatar_url + + +class FanSerializer(serializers.ModelSerializer): + full_name = serializers.CharField(source='get_full_name', required=False) + + class Meta: + model = User + fields = ('id', 'username', 'full_name') diff --git a/taiga/projects/likes/services.py b/taiga/projects/likes/services.py new file mode 100644 index 00000000..f9b94a7a --- /dev/null +++ b/taiga/projects/likes/services.py @@ -0,0 +1,114 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.db.models import F +from django.db.transaction import atomic +from django.apps import apps +from django.contrib.auth import get_user_model + +from .models import Likes, Like + + +def add_like(obj, user): + """Add a like to an object. + + If the user has already liked the object nothing happends, so this function can be considered + idempotent. + + :param obj: Any Django model instance. + :param user: User adding the like. :class:`~taiga.users.models.User` instance. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + with atomic(): + like, created = Like.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user) + if not created: + return + + likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id) + likes.count = F('count') + 1 + likes.save() + return like + + +def remove_like(obj, user): + """Remove an user like from an object. + + If the user has not liked the object nothing happens so this function can be considered + idempotent. + + :param obj: Any Django model instance. + :param user: User removing her like. :class:`~taiga.users.models.User` instance. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + with atomic(): + qs = Like.objects.filter(content_type=obj_type, object_id=obj.id, user=user) + if not qs.exists(): + return + + qs.delete() + + likes, _ = Likes.objects.get_or_create(content_type=obj_type, object_id=obj.id) + likes.count = F('count') - 1 + likes.save() + + +def get_fans(obj): + """Get the fans of an object. + + :param obj: Any Django model instance. + + :return: User queryset object representing the users that liked the object. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + return get_user_model().objects.filter(likes__content_type=obj_type, likes__object_id=obj.id) + + +def get_likes(obj): + """Get the number of likes an object has. + + :param obj: Any Django model instance. + + :return: Number of likes or `0` if the object has no likes at all. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + + try: + return Likes.objects.get(content_type=obj_type, object_id=obj.id).count + except Likes.DoesNotExist: + return 0 + + +def get_liked(user_or_id, model): + """Get the objects liked by an user. + + :param user_or_id: :class:`~taiga.users.models.User` instance or id. + :param model: Show only objects of this kind. Can be any Django model class. + + :return: Queryset of objects representing the likes of the user. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + conditions = ('likes_like.content_type_id = %s', + '%s.id = likes_like.object_id' % model._meta.db_table, + 'likes_like.user_id = %s') + + if isinstance(user_or_id, get_user_model()): + user_id = user_or_id.id + else: + user_id = user_or_id + + return model.objects.extra(where=conditions, tables=('likes_like',), + params=(obj_type.id, user_id)) diff --git a/taiga/projects/likes/utils.py b/taiga/projects/likes/utils.py new file mode 100644 index 00000000..44035d47 --- /dev/null +++ b/taiga/projects/likes/utils.py @@ -0,0 +1,76 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.apps import apps + + +def attach_total_fans_to_queryset(queryset, as_field="total_fans"): + """Attach likes count to each object of the queryset. + + Because of laziness of like objects creation, this makes much simpler and more efficient to + access to liked-object number of likes. + + (The other way was to do it in the serializer with some try/except blocks and additional + queries) + + :param queryset: A Django queryset object. + :param as_field: Attach the likes-count as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = """SELECT coalesce(SUM(total_fans), 0) FROM ( + SELECT coalesce(likes_likes.count, 0) total_fans + FROM likes_likes + WHERE likes_likes.content_type_id = {type_id} + AND likes_likes.object_id = {tbl}.id + ) as e""" + + sql = sql.format(type_id=type.id, tbl=model._meta.db_table) + qs = queryset.extra(select={as_field: sql}) + return qs + + +def attach_is_fan_to_queryset(user, queryset, as_field="is_fan"): + """Attach is_like boolean to each object of the queryset. + + Because of laziness of like objects creation, this makes much simpler and more efficient to + access to likes-object and check if the curren user like it. + + (The other way was to do it in the serializer with some try/except blocks and additional + queries) + + :param user: A users.User object model + :param queryset: A Django queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM likes_like + WHERE likes_like.content_type_id = {type_id} + AND likes_like.object_id = {tbl}.id + AND likes_like.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + qs = queryset.extra(select={as_field: sql}) + return qs diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 96b3c47d..8cc587a0 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -21,7 +21,6 @@ from django.core.management.base import BaseCommand from django.db import transaction from django.utils.timezone import now from django.conf import settings -from django.contrib.webdesign import lorem_ipsum from django.contrib.contenttypes.models import ContentType from sampledatahelper.helper import SampleDataHelper @@ -30,13 +29,18 @@ from taiga.users.models import * from taiga.permissions.permissions import ANON_PERMISSIONS from taiga.projects.models import * from taiga.projects.milestones.models import * +from taiga.projects.notifications.choices import NotifyLevel + 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.history.services import take_snapshot +from taiga.projects.likes.services import add_like +from taiga.projects.votes.services import add_vote from taiga.events.apps import disconnect_events_signals @@ -97,7 +101,9 @@ NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4)) NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20)) NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25)) NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4)) - +NUM_LIKES = getattr(settings, "SAMPLE_DATA_NUM_LIKES", (0, 10)) +NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 10)) +NUM_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 8)) class Command(BaseCommand): sd = SampleDataHelper(seed=12345678901) @@ -119,7 +125,7 @@ class Command(BaseCommand): # create project for x in range(NUM_PROJECTS + NUM_EMPTY_PROJECTS): - project = self.create_project(x) + project = self.create_project(x, is_private=(x in [2, 4] or self.sd.boolean())) # added memberships computable_project_roles = set() @@ -156,18 +162,21 @@ class Command(BaseCommand): for i in range(1, 4): UserStoryCustomAttribute.objects.create(name=self.sd.words(1, 3), description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) if self.sd.boolean: for i in range(1, 4): TaskCustomAttribute.objects.create(name=self.sd.words(1, 3), description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) if self.sd.boolean: for i in range(1, 4): IssueCustomAttribute.objects.create(name=self.sd.words(1, 3), description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) @@ -215,13 +224,14 @@ class Command(BaseCommand): project.total_story_points = int(defined_points * self.sd.int(5,12) / 10) project.save() + self.create_likes(project) def create_attachment(self, obj, order): attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA) membership = self.sd.db_object_from_queryset(obj.project.memberships .filter(user__isnull=False)) attachment = Attachment.objects.create(project=obj.project, - name=path.basename(attached_file.name).lower(), + name=path.basename(attached_file.name), size=attached_file.size, content_object=obj, order=order, @@ -254,6 +264,15 @@ class Command(BaseCommand): return wiki_page + def get_custom_attributes_value(self, type): + if type == TEXT_TYPE: + return self.sd.words(1, 12) + if type == MULTILINE_TYPE: + return self.sd.paragraphs(2, 4) + if type == DATE_TYPE: + return self.sd.future_date(min_distance=0, max_distance=365) + return None + def create_bug(self, project): bug = Issue.objects.create(project=project, subject=self.sd.choice(SUBJECT_CHOICES), @@ -272,8 +291,8 @@ class Command(BaseCommand): bug.save() - custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.issuecustomattributes.all() - if self.sd.boolean()} + custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca + in project.issuecustomattributes.all() if self.sd.boolean()} if custom_attributes_values: bug.custom_attributes_values.attributes_values = custom_attributes_values bug.custom_attributes_values.save() @@ -297,6 +316,9 @@ class Command(BaseCommand): comment=self.sd.paragraph(), user=bug.owner) + self.create_votes(bug) + self.create_watchers(bug) + return bug def create_task(self, project, milestone, us, min_date, max_date, closed=False): @@ -321,8 +343,8 @@ class Command(BaseCommand): task.save() - custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.taskcustomattributes.all() - if self.sd.boolean()} + custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca + in project.taskcustomattributes.all() if self.sd.boolean()} if custom_attributes_values: task.custom_attributes_values.attributes_values = custom_attributes_values task.custom_attributes_values.save() @@ -341,6 +363,9 @@ class Command(BaseCommand): comment=self.sd.paragraph(), user=task.owner) + self.create_votes(task) + self.create_watchers(task) + return task def create_us(self, project, milestone=None, computable_project_roles=[]): @@ -366,8 +391,8 @@ class Command(BaseCommand): us.save() - custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.userstorycustomattributes.all() - if self.sd.boolean()} + custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca + in project.userstorycustomattributes.all() if self.sd.boolean()} if custom_attributes_values: us.custom_attributes_values.attributes_values = custom_attributes_values us.custom_attributes_values.save() @@ -377,9 +402,11 @@ class Command(BaseCommand): attachment = self.create_attachment(us, i+1) if self.sd.choice([True, True, False, True, True]): - us.assigned_to = self.sd.db_object_from_queryset(project.memberships.filter(user__isnull=False)).user + us.assigned_to = self.sd.db_object_from_queryset(project.memberships.filter( + user__isnull=False)).user us.save() + take_snapshot(us, comment=self.sd.paragraph(), user=us.owner) @@ -391,6 +418,9 @@ class Command(BaseCommand): comment=self.sd.paragraph(), user=us.owner) + self.create_votes(us) + self.create_watchers(us) + return us def create_milestone(self, project, start_date, end_date): @@ -409,20 +439,30 @@ class Command(BaseCommand): return milestone - def create_project(self, counter): - is_private=self.sd.boolean() + def create_project(self, counter, is_private=None): + if is_private is None: + is_private=self.sd.boolean() + anon_permissions = not is_private and list(map(lambda perm: perm[0], ANON_PERMISSIONS)) or [] public_permissions = not is_private and list(map(lambda perm: perm[0], ANON_PERMISSIONS)) or [] - project = Project.objects.create(name='Project Example {0}'.format(counter), + project = Project.objects.create(slug='project-%s'%(counter), + name='Project Example {0}'.format(counter), description='Project example {0} description'.format(counter), owner=random.choice(self.users), is_private=is_private, anon_permissions=anon_permissions, public_permissions=public_permissions, total_story_points=self.sd.int(600, 3000), - total_milestones=self.sd.int(5,10)) + total_milestones=self.sd.int(5,10), + tags=self.sd.words(1, 10).split(" ")) + project.is_kanban_activated = True + project.save() take_snapshot(project, user=project.owner) + + self.create_likes(project) + self.create_watchers(project, NotifyLevel.involved) + return project def create_user(self, counter=None, username=None, full_name=None, email=None): @@ -441,3 +481,22 @@ class Command(BaseCommand): user.save() return user + + def create_votes(self, obj): + for i in range(self.sd.int(*NUM_VOTES)): + user=self.sd.db_object_from_queryset(User.objects.all()) + add_vote(obj, user) + + def create_likes(self, obj): + for i in range(self.sd.int(*NUM_LIKES)): + user=self.sd.db_object_from_queryset(User.objects.all()) + add_like(obj, user) + + def create_watchers(self, obj, notify_level=None): + for i in range(self.sd.int(*NUM_WATCHERS)): + user = self.sd.db_object_from_queryset(User.objects.all()) + if not notify_level: + obj.add_watcher(user) + else: + obj.add_watcher(user, notify_level) + diff --git a/taiga/projects/migrations/0013_auto_20141210_1040.py b/taiga/projects/migrations/0013_auto_20141210_1040.py index 93c093bc..68bbb4f5 100644 --- a/taiga/projects/migrations/0013_auto_20141210_1040.py +++ b/taiga/projects/migrations/0013_auto_20141210_1040.py @@ -21,7 +21,6 @@ def _fix_tags_model(tags_model): def fix_tags(apps, schema_editor): - print("Fixing project tags") _fix_tags_model(Project) diff --git a/taiga/projects/migrations/0022_auto_20150701_0924.py b/taiga/projects/migrations/0022_auto_20150701_0924.py new file mode 100644 index 00000000..83d7a337 --- /dev/null +++ b/taiga/projects/migrations/0022_auto_20150701_0924.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0021_auto_20150504_1524'), + ] + + operations = [ + migrations.RenameField( + model_name='projecttemplate', + old_name='videoconferences_salt', + new_name='videoconferences_extra_data', + ), + migrations.RenameField( + model_name='project', + old_name='videoconferences_salt', + new_name='videoconferences_extra_data', + ), + migrations.AlterField( + model_name='project', + name='videoconferences', + field=models.CharField(blank=True, verbose_name='videoconference system', choices=[('appear-in', 'AppearIn'), ('jitsi', 'Jitsi'), ('custom', 'Custom'), ('talky', 'Talky')], null=True, max_length=250), + preserve_default=True, + ), + migrations.AlterField( + model_name='projecttemplate', + name='videoconferences', + field=models.CharField(blank=True, verbose_name='videoconference system', choices=[('appear-in', 'AppearIn'), ('jitsi', 'Jitsi'), ('custom', 'Custom'), ('talky', 'Talky')], null=True, max_length=250), + preserve_default=True, + ), + ] diff --git a/taiga/projects/migrations/0023_auto_20150721_1511.py b/taiga/projects/migrations/0023_auto_20150721_1511.py new file mode 100644 index 00000000..0762e1d0 --- /dev/null +++ b/taiga/projects/migrations/0023_auto_20150721_1511.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0022_auto_20150701_0924'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='videoconferences_extra_data', + field=models.CharField(max_length=250, blank=True, null=True, verbose_name='videoconference extra data'), + preserve_default=True, + ), + migrations.AlterField( + model_name='projecttemplate', + name='videoconferences_extra_data', + field=models.CharField(max_length=250, blank=True, null=True, verbose_name='videoconference extra data'), + preserve_default=True, + ), + ] diff --git a/taiga/projects/migrations/0024_auto_20150810_1247.py b/taiga/projects/migrations/0024_auto_20150810_1247.py new file mode 100644 index 00000000..f057816b --- /dev/null +++ b/taiga/projects/migrations/0024_auto_20150810_1247.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import djorm_pgarray.fields +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0023_auto_20150721_1511'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='public_permissions', + field=djorm_pgarray.fields.TextArrayField(default=[], dbtype='text', choices=[('view_project', 'View project'), ('star_project', 'Star project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('vote_us', 'Vote user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('vote_task', 'Vote task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('vote_issue', 'Vote issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], verbose_name='user permissions'), + preserve_default=True, + ), + ] diff --git a/taiga/projects/migrations/0025_auto_20150901_1600.py b/taiga/projects/migrations/0025_auto_20150901_1600.py new file mode 100644 index 00000000..8859b14e --- /dev/null +++ b/taiga/projects/migrations/0025_auto_20150901_1600.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import djorm_pgarray.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0024_auto_20150810_1247'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='public_permissions', + field=djorm_pgarray.fields.TextArrayField(default=[], choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', verbose_name='user permissions'), + preserve_default=True, + ), + ] diff --git a/taiga/projects/migrations/0026_auto_20150911_1237.py b/taiga/projects/migrations/0026_auto_20150911_1237.py new file mode 100644 index 00000000..073c2349 --- /dev/null +++ b/taiga/projects/migrations/0026_auto_20150911_1237.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import connection +from django.db import migrations + + +def create_postgres_search_dictionary(apps, schema_editor): + sql=""" +CREATE TEXT SEARCH DICTIONARY english_stem_nostop ( + Template = snowball, + Language = english +); +CREATE TEXT SEARCH CONFIGURATION public.english_nostop ( COPY = pg_catalog.english ); +ALTER TEXT SEARCH CONFIGURATION public.english_nostop +ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, hword, hword_part, word WITH english_stem_nostop; +""" + cursor = connection.cursor() + cursor.execute(sql) + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0025_auto_20150901_1600'), + ] + + operations = [ + migrations.RunPython(create_postgres_search_dictionary), + ] diff --git a/taiga/projects/migrations/0027_auto_20150916_1302.py b/taiga/projects/migrations/0027_auto_20150916_1302.py new file mode 100644 index 00000000..ecdb0f41 --- /dev/null +++ b/taiga/projects/migrations/0027_auto_20150916_1302.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0026_auto_20150911_1237'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='total_milestones', + field=models.IntegerField(verbose_name='total of milestones', null=True, blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='project', + name='total_story_points', + field=models.FloatField(verbose_name='total story points', null=True, blank=True), + preserve_default=True, + ), + ] diff --git a/taiga/projects/milestones/admin.py b/taiga/projects/milestones/admin.py index b741a1b2..378928ce 100644 --- a/taiga/projects/milestones/admin.py +++ b/taiga/projects/milestones/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,6 +15,8 @@ # along with this program. If not, see . from django.contrib import admin +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline from . import models @@ -30,6 +32,7 @@ class MilestoneAdmin(admin.ModelAdmin): list_display_links = list_display list_filter = ["project"] readonly_fields = ["owner"] + inlines = [WatchedInline, VoteInline] admin.site.register(models.Milestone, MilestoneAdmin) diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 132f9bf2..d9e044be 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -14,16 +14,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps + from taiga.base import filters from taiga.base import response from taiga.base.decorators import detail_route -from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.utils import get_object_or_404 +from taiga.base.utils.db import get_object_or_none -from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin - from . import serializers from . import models from . import permissions @@ -36,17 +38,37 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView permission_classes = (permissions.MilestonePermission,) filter_backends = (filters.CanViewMilestonesFilterBackend,) filter_fields = ("project", "closed") + queryset = models.Milestone.objects.all() + + def list(self, request, *args, **kwargs): + res = super().list(request, *args, **kwargs) + self._add_taiga_info_headers() + return res + + def _add_taiga_info_headers(self): + try: + project_id = int(self.request.QUERY_PARAMS.get("project", None)) + project_model = apps.get_model("projects", "Project") + project = get_object_or_none(project_model, id=project_id) + except TypeError: + project = None + + if project: + opened_milestones = project.milestones.filter(closed=False).count() + closed_milestones = project.milestones.filter(closed=True).count() + + self.headers["Taiga-Info-Total-Opened-Milestones"] = opened_milestones + self.headers["Taiga-Info-Total-Closed-Milestones"] = closed_milestones def get_queryset(self): - qs = models.Milestone.objects.all() + qs = super().get_queryset() + qs = self.attach_watchers_attrs_to_queryset(qs) qs = qs.prefetch_related("user_stories", "user_stories__role_points", "user_stories__role_points__points", "user_stories__role_points__role", "user_stories__generated_from_issue", - "user_stories__project", - "watchers", - "user_stories__watchers") + "user_stories__project") qs = qs.select_related("project") qs = qs.order_by("-estimated_start") return qs @@ -93,3 +115,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView optimal_points -= optimal_points_per_day return response.Ok(milestone_stats) + + +class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.MilestoneWatchersPermission,) + resource_model = models.Milestone diff --git a/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py b/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py new file mode 100644 index 00000000..0d8a3a61 --- /dev/null +++ b/taiga/projects/milestones/migrations/0002_remove_milestone_watchers.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import connection +from django.db import models, migrations +from django.contrib.contenttypes.models import ContentType +from taiga.base.utils.contenttypes import update_all_contenttypes + +def create_notifications(apps, schema_editor): + update_all_contenttypes(verbosity=0) + sql=""" +INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id) +SELECT milestone_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id +FROM milestones_milestone_watchers INNER JOIN milestones_milestone ON milestones_milestone_watchers.milestone_id = milestones_milestone.id""".format(content_type_id=ContentType.objects.get(model='milestone').id) + cursor = connection.cursor() + cursor.execute(sql) + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_watched'), + ('milestones', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_notifications), + migrations.RemoveField( + model_name='milestone', + name='watchers', + ), + ] diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index 7e18048e..0e380c9b 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py index 9823c8de..c088d9a9 100644 --- a/taiga/projects/milestones/permissions.py +++ b/taiga/projects/milestones/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,8 +15,8 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectOwner, AllowAny, - PermissionComponent, IsSuperUser) + IsAuthenticated, IsProjectOwner, AllowAny, + IsSuperUser) class MilestonePermission(TaigaResourcePermission): @@ -29,3 +29,11 @@ class MilestonePermission(TaigaResourcePermission): destroy_perms = HasProjectPerm('delete_milestone') list_perms = AllowAny() stats_perms = HasProjectPerm('view_milestones') + watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones') + +class MilestoneWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_milestones') + list_perms = HasProjectPerm('view_milestones') diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 2ffd1d43..50e90a49 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,14 +17,15 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers - from taiga.base.utils import json +from taiga.projects.notifications.mixins import WatchedResourceModelSerializer +from taiga.projects.notifications.validators import WatchersValidator from ..userstories.serializers import UserStorySerializer from . import models -class MilestoneSerializer(serializers.ModelSerializer): +class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer): user_stories = UserStorySerializer(many=True, required=False, read_only=True) total_points = serializers.SerializerMethodField("get_total_points") closed_points = serializers.SerializerMethodField("get_closed_points") diff --git a/taiga/projects/milestones/services.py b/taiga/projects/milestones/services.py index a94be521..f852403f 100644 --- a/taiga/projects/milestones/services.py +++ b/taiga/projects/milestones/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/mixins/blocked.py b/taiga/projects/mixins/blocked.py index 43ce45e7..34db95b1 100644 --- a/taiga/projects/mixins/blocked.py +++ b/taiga/projects/mixins/blocked.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/mixins/on_destroy.py b/taiga/projects/mixins/on_destroy.py index 6fe69c7b..6ba1c40f 100644 --- a/taiga/projects/mixins/on_destroy.py +++ b/taiga/projects/mixins/on_destroy.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/mixins/ordering.py b/taiga/projects/mixins/ordering.py index b180bc1f..29723fd3 100644 --- a/taiga/projects/mixins/ordering.py +++ b/taiga/projects/mixins/ordering.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 64fff2a9..1bc8ab4d 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -20,7 +20,7 @@ import uuid from django.core.exceptions import ValidationError from django.db import models -from django.db.models import signals +from django.db.models import signals, Q from django.apps import apps from django.conf import settings from django.dispatch import receiver @@ -38,6 +38,13 @@ from taiga.base.utils.dicts import dict_sum from taiga.base.utils.sequence import arithmetic_progression from taiga.base.utils.slug import slugify_uniquely_for_queryset +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.services import ( + get_notify_policy, + set_notify_policy_level, + set_notify_policy_level_to_ignore, + create_notify_policy_if_not_exists) + from . import choices @@ -71,6 +78,10 @@ class Membership(models.Model): user_order = models.IntegerField(default=10000, null=False, blank=False, verbose_name=_("user order")) + def get_related_people(self): + related_people = get_user_model().objects.filter(id=self.user.id) + return related_people + def clean(self): # TODO: Review and do it more robust memberships = Membership.objects.filter(user=self.user, project=self.project) @@ -135,9 +146,9 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects", through="Membership", verbose_name=_("members"), through_fields=("project", "user")) - total_milestones = models.IntegerField(default=0, null=False, blank=False, + total_milestones = models.IntegerField(null=True, blank=True, verbose_name=_("total of milestones")) - total_story_points = models.FloatField(default=0, verbose_name=_("total story points")) + total_story_points = models.FloatField(null=True, blank=True, verbose_name=_("total story points")) is_backlog_activated = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("active backlog panel")) @@ -150,8 +161,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): videoconferences = models.CharField(max_length=250, null=True, blank=True, choices=choices.VIDEOCONFERENCES_CHOICES, verbose_name=_("videoconference system")) - videoconferences_salt = models.CharField(max_length=250, null=True, blank=True, - verbose_name=_("videoconference room salt")) + videoconferences_extra_data = models.CharField(max_length=250, null=True, blank=True, + verbose_name=_("videoconference extra data")) creation_template = models.ForeignKey("projects.ProjectTemplate", related_name="projects", null=True, @@ -209,7 +220,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): self.slug = slug if not self.videoconferences: - self.videoconferences_salt = None + self.videoconferences_extra_data = None super().save(*args, **kwargs) @@ -332,6 +343,39 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): "assigned": self._get_user_stories_points(assigned_user_stories), } + def _get_q_watchers(self): + return Q(notify_policies__project_id=self.id) & ~Q(notify_policies__notify_level=NotifyLevel.none) + + def get_watchers(self): + return get_user_model().objects.filter(self._get_q_watchers()) + + def get_related_people(self): + related_people_q = Q() + + ## - Owner + if self.owner_id: + related_people_q.add(Q(id=self.owner_id), Q.OR) + + ## - Watchers + related_people_q.add(self._get_q_watchers(), Q.OR) + + ## - Apply filters + related_people = get_user_model().objects.filter(related_people_q) + + ## - Exclude inactive and system users and remove duplicate + related_people = related_people.exclude(is_active=False) + related_people = related_people.exclude(is_system=True) + related_people = related_people.distinct() + return related_people + + def add_watcher(self, user, notify_level=NotifyLevel.all): + notify_policy = create_notify_policy_if_not_exists(self, user) + set_notify_policy_level(notify_policy, notify_level) + + def remove_watcher(self, user): + notify_policy = get_notify_policy(self, user) + set_notify_policy_level_to_ignore(notify_policy) + class ProjectModulesConfig(models.Model): project = models.OneToOneField("Project", null=False, blank=False, @@ -577,8 +621,8 @@ class ProjectTemplate(models.Model): videoconferences = models.CharField(max_length=250, null=True, blank=True, choices=choices.VIDEOCONFERENCES_CHOICES, verbose_name=_("videoconference system")) - videoconferences_salt = models.CharField(max_length=250, null=True, blank=True, - verbose_name=_("videoconference room salt")) + videoconferences_extra_data = models.CharField(max_length=250, null=True, blank=True, + verbose_name=_("videoconference extra data")) default_options = JsonField(null=True, blank=True, verbose_name=_("default options")) us_statuses = JsonField(null=True, blank=True, verbose_name=_("us statuses")) @@ -613,7 +657,7 @@ class ProjectTemplate(models.Model): self.is_wiki_activated = project.is_wiki_activated self.is_issues_activated = project.is_issues_activated self.videoconferences = project.videoconferences - self.videoconferences_salt = project.videoconferences_salt + self.videoconferences_extra_data = project.videoconferences_extra_data self.default_options = { "points": getattr(project.default_points, "name", None), @@ -717,7 +761,7 @@ class ProjectTemplate(models.Model): project.is_wiki_activated = self.is_wiki_activated project.is_issues_activated = self.is_issues_activated project.videoconferences = self.videoconferences - project.videoconferences_salt = self.videoconferences_salt + project.videoconferences_extra_data = self.videoconferences_extra_data for us_status in self.us_statuses: UserStoryStatus.objects.create( diff --git a/taiga/projects/notifications/admin.py b/taiga/projects/notifications/admin.py new file mode 100644 index 00000000..a2278932 --- /dev/null +++ b/taiga/projects/notifications/admin.py @@ -0,0 +1,25 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline + +from . import models + + +class WatchedInline(GenericTabularInline): + model = models.Watched + extra = 0 diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py index 2431c7c2..26b8440f 100644 --- a/taiga/projects/notifications/api.py +++ b/taiga/projects/notifications/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -19,8 +19,9 @@ from django.db.models import Q from taiga.base.api import ModelCrudViewSet from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.models import Watched from taiga.projects.models import Project - +from taiga.users import services as user_services from . import serializers from . import models from . import permissions @@ -38,12 +39,14 @@ class NotifyPolicyViewSet(ModelCrudViewSet): ).distinct() for project in projects: - services.create_notify_policy_if_not_exists(project, self.request.user, NotifyLevel.watch) + services.create_notify_policy_if_not_exists(project, self.request.user, NotifyLevel.all) def get_queryset(self): if self.request.user.is_anonymous(): return models.NotifyPolicy.objects.none() self._build_needed_notify_policies() - qs = models.NotifyPolicy.objects.filter(user=self.request.user) - return qs.distinct() + + return models.NotifyPolicy.objects.filter(user=self.request.user).filter( + Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user) + ).distinct() diff --git a/taiga/projects/notifications/choices.py b/taiga/projects/notifications/choices.py index 04e28d89..e42d9c91 100644 --- a/taiga/projects/notifications/choices.py +++ b/taiga/projects/notifications/choices.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -19,13 +19,13 @@ from django.utils.translation import ugettext_lazy as _ class NotifyLevel(enum.IntEnum): - notwatch = 1 - watch = 2 - ignore = 3 + involved = 1 + all = 2 + none = 3 NOTIFY_LEVEL_CHOICES = ( - (NotifyLevel.notwatch, _("Not watching")), - (NotifyLevel.watch, _("Watching")), - (NotifyLevel.ignore, _("Ignoring")), + (NotifyLevel.involved, _("Involved")), + (NotifyLevel.all, _("All")), + (NotifyLevel.none, _("None")), ) diff --git a/taiga/projects/notifications/management/commands/send_notifications.py b/taiga/projects/notifications/management/commands/send_notifications.py index 4f2e4643..5aac7558 100644 --- a/taiga/projects/notifications/management/commands/send_notifications.py +++ b/taiga/projects/notifications/management/commands/send_notifications.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/notifications/migrations/0004_watched.py b/taiga/projects/notifications/migrations/0004_watched.py new file mode 100644 index 00000000..ab0878b7 --- /dev/null +++ b/taiga/projects/notifications/migrations/0004_watched.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0001_initial'), + ('notifications', '0003_auto_20141029_1143'), + ] + + operations = [ + migrations.CreateModel( + name='Watched', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('object_id', models.PositiveIntegerField()), + ('created_date', models.DateTimeField(verbose_name='created date', auto_now_add=True)), + ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ('user', models.ForeignKey(related_name='watched', verbose_name='user', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='watched')), + + ], + options={ + 'verbose_name': 'Watched', + 'verbose_name_plural': 'Watched', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='watched', + unique_together=set([('content_type', 'object_id', 'user', 'project')]), + ), + ] diff --git a/taiga/projects/notifications/migrations/0005_auto_20151005_1357.py b/taiga/projects/notifications/migrations/0005_auto_20151005_1357.py new file mode 100644 index 00000000..3d38d5e6 --- /dev/null +++ b/taiga/projects/notifications/migrations/0005_auto_20151005_1357.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_watched'), + ] + + operations = [ + migrations.AlterField( + model_name='historychangenotification', + name='history_entries', + field=models.ManyToManyField(verbose_name='history entries', to='history.HistoryEntry', related_name='+'), + ), + migrations.AlterField( + model_name='historychangenotification', + name='notify_users', + field=models.ManyToManyField(verbose_name='notify users', to=settings.AUTH_USER_MODEL, related_name='+'), + ), + ] diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 362635a4..a147431b 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,14 +17,29 @@ from functools import partial from operator import is_not -from django.conf import settings +from django.apps import apps +from django.contrib.contenttypes.models import ContentType +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 +from taiga.base.api import serializers +from taiga.base.api.utils import get_object_or_404 +from taiga.base.fields import WatchersField from taiga.projects.notifications import services +from taiga.projects.notifications.utils import (attach_watchers_to_queryset, + attach_is_watcher_to_queryset, + attach_total_watchers_to_queryset) + +from taiga.users.models import User +from . import models +from . serializers import WatcherSerializer -class WatchedResourceMixin(object): + +class WatchedResourceMixin: """ Rest Framework resource mixin for resources susceptible to be notifiable about their changes. @@ -36,6 +51,28 @@ class WatchedResourceMixin(object): _not_notify = False + def attach_watchers_attrs_to_queryset(self, queryset): + qs = attach_watchers_to_queryset(queryset) + qs = attach_total_watchers_to_queryset(queryset) + if self.request.user.is_authenticated(): + qs = attach_is_watcher_to_queryset(qs, self.request.user) + + return qs + + @detail_route(methods=["POST"]) + def watch(self, request, pk=None): + obj = self.get_object() + self.check_permissions(request, "watch", obj) + services.add_watcher(obj, request.user) + return response.Ok() + + @detail_route(methods=["POST"]) + def unwatch(self, request, pk=None): + obj = self.get_object() + self.check_permissions(request, "unwatch", obj) + services.remove_watcher(obj, request.user) + return response.Ok() + def send_notifications(self, obj, history=None): """ Shortcut method for resources with special save @@ -58,7 +95,7 @@ class WatchedResourceMixin(object): # some text fields for extract mentions and add them # to watchers before obtain a complete list of # notifiable users. - services.analize_object_for_watchers(obj, history) + services.analize_object_for_watchers(obj, history.comment, history.owner) # Get a complete list of notifiable users for current # object and send the change notification to them. @@ -73,7 +110,7 @@ class WatchedResourceMixin(object): super().pre_delete(obj) -class WatchedModelMixin(models.Model): +class WatchedModelMixin(object): """ Generic model mixin that makes model compatible with notification system. @@ -82,11 +119,6 @@ class WatchedModelMixin(models.Model): this mixin if you want send notifications about your model class. """ - watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True, - related_name="%(app_label)s_%(class)s+", - verbose_name=_("watchers")) - class Meta: - abstract = True def get_project(self) -> object: """ @@ -98,21 +130,28 @@ class WatchedModelMixin(models.Model): """ return self.project - def get_watchers(self) -> frozenset: + def get_watchers(self) -> object: """ Default implementation method for obtain a list of watchers for current instance. - - NOTE: the default implementation returns frozen - set of all watchers if "watchers" attribute exists - in a model. - - WARNING: it returns a full evaluated set and in - future, for project with 1000k watchers it can be - very inefficient way for obtain watchers but at - this momment is the simplest way. """ - return frozenset(self.watchers.all()) + return services.get_watchers(self) + + def get_related_people(self) -> object: + """ + Default implementation for obtain the related people of + current instance. + """ + return services.get_related_people(self) + + def get_watched(self, user_or_id): + return services.get_watched(user_or_id, type(self)) + + def add_watcher(self, user): + services.add_watcher(self, user) + + def remove_watcher(self, user): + services.remove_watcher(self, user) def get_owner(self) -> object: """ @@ -140,3 +179,103 @@ class WatchedModelMixin(models.Model): self.get_owner(),) is_not_none = partial(is_not, None) return frozenset(filter(is_not_none, participants)) + + +class WatchedResourceModelSerializer(serializers.ModelSerializer): + is_watcher = serializers.SerializerMethodField("get_is_watcher") + total_watchers = serializers.SerializerMethodField("get_total_watchers") + + def get_is_watcher(self, obj): + # The "is_watcher" attribute is attached in the get_queryset of the viewset. + return getattr(obj, "is_watcher", False) or False + + def get_total_watchers(self, obj): + # The "total_watchers" attribute is attached in the get_queryset of the viewset. + return getattr(obj, "total_watchers", 0) or 0 + + +class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): + watchers = WatchersField(required=False) + + def restore_object(self, attrs, instance=None): + #watchers is not a field from the model but can be attached in the get_queryset of the viewset. + #If that's the case we need to remove it before calling the super method + watcher_field = self.fields.pop("watchers", None) + self.validate_watchers(attrs, "watchers") + new_watcher_ids = attrs.pop("watchers", None) + obj = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance) + + #A partial update can exclude the watchers field or if the new instance can still not be saved + if instance is None or new_watcher_ids is None: + return obj + + new_watcher_ids = set(new_watcher_ids) + old_watcher_ids = set(obj.get_watchers().values_list("id", flat=True)) + 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) + for user in adding_users: + services.add_watcher(obj, user) + + for user in removing_users: + services.remove_watcher(obj, user) + + obj.watchers = obj.get_watchers() + + return obj + + def to_native(self, obj): + #if watchers wasn't attached via the get_queryset of the viewset we need to manually add it + if obj is not None and 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 + + return super(WatchedResourceModelSerializer, self).to_native(obj) + + def save(self, **kwargs): + obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs) + self.fields["watchers"] = WatchersField(required=False) + obj.watchers = [user.id for user in obj.get_watchers()] + return obj + + +class WatchersViewSetMixin: + # Is a ModelListViewSet with two required params: permission_classes and resource_model + serializer_class = WatcherSerializer + list_serializer_class = WatcherSerializer + permission_classes = None + resource_model = None + + def retrieve(self, request, *args, **kwargs): + pk = kwargs.get("pk", None) + resource_id = kwargs.get("resource_id", None) + resource = get_object_or_404(self.resource_model, pk=resource_id) + + self.check_permissions(request, 'retrieve', resource) + + try: + self.object = resource.get_watchers().get(pk=pk) + except ObjectDoesNotExist: # or User.DoesNotExist + return response.NotFound() + + serializer = self.get_serializer(self.object) + return response.Ok(serializer.data) + + def list(self, request, *args, **kwargs): + resource_id = kwargs.get("resource_id", None) + resource = get_object_or_404(self.resource_model, pk=resource_id) + + self.check_permissions(request, 'list', resource) + + return super().list(request, *args, **kwargs) + + def get_queryset(self): + resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id")) + return resource.get_watchers() diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py index 29983f90..6ce2356b 100644 --- a/taiga/projects/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -14,13 +14,15 @@ # 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.contenttypes import generic from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from taiga.projects.history.choices import HISTORY_TYPE_CHOICES -from .choices import NOTIFY_LEVEL_CHOICES +from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel class NotifyPolicy(models.Model): @@ -59,10 +61,10 @@ class HistoryChangeNotification(models.Model): verbose_name=_("created date time")) updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, verbose_name=_("updated date time")) - history_entries = models.ManyToManyField("history.HistoryEntry", null=True, blank=True, + history_entries = models.ManyToManyField("history.HistoryEntry", verbose_name=_("history entries"), related_name="+") - notify_users = models.ManyToManyField("users.User", null=True, blank=True, + notify_users = models.ManyToManyField("users.User", verbose_name=_("notify users"), related_name="+") project = models.ForeignKey("projects.Project", null=False, blank=False, @@ -72,3 +74,19 @@ class HistoryChangeNotification(models.Model): class Meta: unique_together = ("key", "owner", "project", "history_type") + + +class Watched(models.Model): + content_type = models.ForeignKey("contenttypes.ContentType") + object_id = models.PositiveIntegerField() + content_object = generic.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, + verbose_name=_("created date")) + project = models.ForeignKey("projects.Project", null=False, blank=False, + verbose_name=_("project"),related_name="watched") + class Meta: + verbose_name = _("Watched") + verbose_name_plural = _("Watched") + unique_together = ("content_type", "object_id", "user", "project") diff --git a/taiga/projects/notifications/permissions.py b/taiga/projects/notifications/permissions.py index a89b9caf..699e0a4a 100644 --- a/taiga/projects/notifications/permissions.py +++ b/taiga/projects/notifications/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/notifications/serializers.py b/taiga/projects/notifications/serializers.py index c60e4bc9..e0c988b5 100644 --- a/taiga/projects/notifications/serializers.py +++ b/taiga/projects/notifications/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,9 +17,10 @@ import json from taiga.base.api import serializers +from taiga.users.models import User from . import models - +from . import choices class NotifyPolicySerializer(serializers.ModelSerializer): @@ -31,3 +32,11 @@ class NotifyPolicySerializer(serializers.ModelSerializer): def get_project_name(self, obj): return obj.project.name + + +class WatcherSerializer(serializers.ModelSerializer): + full_name = serializers.CharField(source='get_full_name', required=False) + + class Meta: + model = User + fields = ('id', 'username', 'full_name') diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 67fb894a..2be1c8fc 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -14,20 +14,22 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import datetime + from functools import partial from django.apps import apps -from django.db import IntegrityError +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 +from django.contrib.auth import get_user_model from django.utils import timezone -from django.db import transaction from django.conf import settings from django.utils.translation import ugettext as _ -from djmail import template_mail - from taiga.base import exceptions as exc -from taiga.base.utils.text import strip_lines +from taiga.base.mails import InlineCSSTemplateMail from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.history.choices import HistoryType from taiga.projects.history.services import (make_key_from_model_object, @@ -36,7 +38,7 @@ from taiga.projects.history.services import (make_key_from_model_object, from taiga.permissions.service import user_has_perm from taiga.users.models import User -from .models import HistoryChangeNotification +from .models import HistoryChangeNotification, Watched def notify_policy_exists(project, user) -> bool: @@ -50,7 +52,7 @@ def notify_policy_exists(project, user) -> bool: return qs.exists() -def create_notify_policy(project, user, level=NotifyLevel.notwatch): +def create_notify_policy(project, user, level=NotifyLevel.involved): """ Given a project and user, create notification policy for it. """ @@ -63,7 +65,7 @@ def create_notify_policy(project, user, level=NotifyLevel.notwatch): raise exc.IntegrityError(_("Notify exists for specified user and project")) from e -def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.notwatch): +def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.involved): """ Given a project and user, create notification policy for it. """ @@ -83,45 +85,37 @@ def get_notify_policy(project, user): """ model_cls = apps.get_model("notifications", "NotifyPolicy") instance, _ = model_cls.objects.get_or_create(project=project, user=user, - defaults={"notify_level": NotifyLevel.notwatch}) + defaults={"notify_level": NotifyLevel.involved}) return instance -def attach_notify_policy_to_project_queryset(current_user, queryset): - """ - Function that attach "notify_level" attribute on each queryset - result for query notification level of current user for each - project in the most efficient way. - """ - - sql = strip_lines(""" - COALESCE((SELECT notifications_notifypolicy.notify_level - FROM notifications_notifypolicy - WHERE notifications_notifypolicy.project_id = projects_project.id - AND notifications_notifypolicy.user_id = {userid}), {default_level}) - """) - - sql = sql.format(userid=current_user.pk, - default_level=NotifyLevel.notwatch) - return queryset.extra(select={"notify_level": sql}) - - -def analize_object_for_watchers(obj:object, history:object): +def analize_object_for_watchers(obj:object, comment:str, user:object): """ Generic implementation for analize model objects and extract mentions from it and add it to watchers. """ + + if not hasattr(obj, "get_project"): + return + + if not hasattr(obj, "add_watcher"): + return + from taiga import mdrender as mdr texts = (getattr(obj, "description", ""), getattr(obj, "content", ""), - getattr(history, "comment", ""),) + comment,) _, data = mdr.render_and_extract(obj.get_project(), "\n".join(texts)) if data["mentions"]: for user in data["mentions"]: - obj.watchers.add(user) + obj.add_watcher(user) + + # Adding the person who edited the object to the watchers + if comment and not user.is_system: + obj.add_watcher(user) def _filter_by_permissions(obj, user): @@ -160,22 +154,23 @@ def get_users_to_notify(obj, *, discard_users=None) -> list: return policy.notify_level in [int(x) for x in levels] _can_notify_hard = partial(_check_level, project, - levels=[NotifyLevel.watch]) + levels=[NotifyLevel.all]) _can_notify_light = partial(_check_level, project, - levels=[NotifyLevel.watch, NotifyLevel.notwatch]) + levels=[NotifyLevel.all, NotifyLevel.involved]) candidates = set() candidates.update(filter(_can_notify_hard, project.members.all())) candidates.update(filter(_can_notify_light, obj.get_watchers())) + candidates.update(filter(_can_notify_light, obj.project.get_watchers())) candidates.update(filter(_can_notify_light, obj.get_participants())) # Remove the changer from candidates if discard_users: candidates = candidates - set(discard_users) - candidates = filter(partial(_filter_by_permissions, obj), candidates) + candidates = set(filter(partial(_filter_by_permissions, obj), candidates)) # Filter disabled and system users - candidates = filter(partial(_filter_notificable), candidates) + candidates = set(filter(partial(_filter_notificable), candidates)) return frozenset(candidates) @@ -206,7 +201,7 @@ def _make_template_mail(name:str): of it. """ cls = type("InlineCSSTemplateMail", - (template_mail.InlineCSSTemplateMail,), + (InlineCSSTemplateMail,), {"name": name}) return cls() @@ -248,7 +243,7 @@ def send_sync_notifications(notification_id): """ notification = HistoryChangeNotification.objects.select_for_update().get(pk=notification_id) - # If the las modification is too recent we ignore it + # If the last modification is too recent we ignore it now = timezone.now() time_diff = now - notification.updated_datetime if time_diff.seconds < settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL: @@ -267,11 +262,34 @@ def send_sync_notifications(notification_id): model = get_model_from_key(notification.key) template_name = _resolve_template_name(model, change_type=notification.history_type) email = _make_template_mail(template_name) + domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"] + + if "ref" in obj.snapshot: + msg_id = obj.snapshot["ref"] + elif "slug" in obj.snapshot: + msg_id = obj.snapshot["slug"] + else: + msg_id = 'taiga-system' + + now = datetime.datetime.now() + format_args = {"project_slug": notification.project.slug, + "project_name": notification.project.name, + "msg_id": msg_id, + "time": int(now.timestamp()), + "domain": domain} + + headers = {"Message-ID": "<{project_slug}/{msg_id}/{time}@{domain}>".format(**format_args), + "In-Reply-To": "<{project_slug}/{msg_id}@{domain}>".format(**format_args), + "References": "<{project_slug}/{msg_id}@{domain}>".format(**format_args), + + "List-ID": 'Taiga/{project_name} '.format(**format_args), + + "Thread-Index": make_ms_thread_index("<{project_slug}/{msg_id}@{domain}>".format(**format_args), now)} for user in notification.notify_users.distinct(): context["user"] = user context["lang"] = user.lang or settings.LANGUAGE_CODE - email.send(user.email, context) + email.send(user.email, context, headers=headers) notification.delete() @@ -279,3 +297,171 @@ def send_sync_notifications(notification_id): def process_sync_notifications(): for notification in HistoryChangeNotification.objects.all(): send_sync_notifications(notification.pk) + + +def _get_q_watchers(obj): + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + return Q(watched__content_type=obj_type, watched__object_id=obj.id) + + +def get_watchers(obj): + """Get the watchers of an object. + + :param obj: Any Django model instance. + + :return: User queryset object representing the users that watch the object. + """ + return get_user_model().objects.filter(_get_q_watchers(obj)) + + +def get_related_people(obj): + """Get the related people of an object for notifications. + + :param obj: Any Django model instance. + + :return: User queryset object representing the users related to the object. + """ + related_people_q = Q() + + ## - Owner + if hasattr(obj, "owner_id") and obj.owner_id: + related_people_q.add(Q(id=obj.owner_id), Q.OR) + + ## - Assigned to + if hasattr(obj, "assigned_to_id") and obj.assigned_to_id: + related_people_q.add(Q(id=obj.assigned_to_id), Q.OR) + + ## - Watchers + related_people_q.add(_get_q_watchers(obj), Q.OR) + + ## - Apply filters + related_people = get_user_model().objects.filter(related_people_q) + + ## - Exclude inactive and system users and remove duplicate + related_people = related_people.exclude(is_active=False) + related_people = related_people.exclude(is_system=True) + related_people = related_people.distinct() + return related_people + + +def get_watched(user_or_id, model): + """Get the objects watched by an user. + + :param user_or_id: :class:`~taiga.users.models.User` instance or id. + :param model: Show only objects of this kind. Can be any Django model class. + + :return: Queryset of objects representing the votes of the user. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + conditions = ('notifications_watched.content_type_id = %s', + '%s.id = notifications_watched.object_id' % model._meta.db_table, + 'notifications_watched.user_id = %s') + + if isinstance(user_or_id, get_user_model()): + user_id = user_or_id.id + else: + user_id = user_or_id + + return model.objects.extra(where=conditions, tables=('notifications_watched',), + params=(obj_type.id, user_id)) + + +def get_projects_watched(user_or_id): + """Get the objects watched by an user. + + :param user_or_id: :class:`~taiga.users.models.User` instance or id. + :param model: Show only objects of this kind. Can be any Django model class. + + :return: Queryset of objects representing the votes of the user. + """ + + if isinstance(user_or_id, get_user_model()): + user_id = user_or_id.id + else: + user_id = user_or_id + + project_class = apps.get_model("projects", "Project") + return project_class.objects.filter(notify_policies__user__id=user_id).exclude(notify_policies__notify_level=NotifyLevel.none) + +def add_watcher(obj, user): + """Add a watcher to an object. + + If the user is already watching the object nothing happents (except if there is a level update), + so this function can be considered idempotent. + + :param obj: Any Django model instance. + :param user: User adding the watch. :class:`~taiga.users.models.User` instance. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + watched, created = Watched.objects.get_or_create(content_type=obj_type, + object_id=obj.id, user=user, project=obj.project) + + notify_policy, _ = apps.get_model("notifications", "NotifyPolicy").objects.get_or_create( + project=obj.project, user=user, defaults={"notify_level": NotifyLevel.all}) + + return watched + + +def remove_watcher(obj, user): + """Remove an watching user from an object. + + If the user has not watched the object nothing happens so this function can be considered + idempotent. + + :param obj: Any Django model instance. + :param user: User removing the watch. :class:`~taiga.users.models.User` instance. + """ + obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) + qs = Watched.objects.filter(content_type=obj_type, object_id=obj.id, user=user) + if not qs.exists(): + return + + qs.delete() + + +def set_notify_policy_level(notify_policy, notify_level): + """ + Set notification level for specified policy. + """ + if not notify_level in [e.value for e in NotifyLevel]: + raise exc.IntegrityError(_("Invalid value for notify level")) + + notify_policy.notify_level = notify_level + notify_policy.save() + + +def set_notify_policy_level_to_ignore(notify_policy): + """ + Set notification level for specified policy. + """ + set_notify_policy_level(notify_policy, NotifyLevel.none) + + +def make_ms_thread_index(msg_id, dt): + """ + Create the 22-byte base of the thread-index string in the format: + + 6 bytes = First 6 significant bytes of the FILETIME stamp + 16 bytes = GUID (we're using a md5 hash of the message id) + + See http://www.meridiandiscovery.com/how-to/e-mail-conversation-index-metadata-computer-forensics/ + """ + + import base64 + import hashlib + import struct + + # Convert to FILETIME epoch (microseconds since 1601) + delta = datetime.date(1970, 1, 1) - datetime.date(1601, 1, 1) + filetime = int(dt.timestamp() + delta.total_seconds()) * 10000000 + + # only want the first 6 bytes + thread_bin = struct.pack(">Q", filetime)[:6] + + # Make a guid. This is usually generated by Outlook. + # The format is usually >IHHQ, but we don't care since it's just a hash of the id + md5 = hashlib.md5(msg_id.encode('utf-8')) + thread_bin += md5.digest() + + # base64 encode + return base64.b64encode(thread_bin) diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py new file mode 100644 index 00000000..49aea78d --- /dev/null +++ b/taiga/projects/notifications/utils.py @@ -0,0 +1,155 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.apps import apps +from .choices import NotifyLevel +from taiga.base.utils.text import strip_lines + +def attach_watchers_to_queryset(queryset, as_field="watchers"): + """Attach watching user ids to each object of the queryset. + + :param queryset: A Django queryset object. + :param as_field: Attach the watchers as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + + sql = ("""SELECT array(SELECT user_id + FROM notifications_watched + WHERE notifications_watched.content_type_id = {type_id} + AND notifications_watched.object_id = {tbl}.id)""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table) + qs = queryset.extra(select={as_field: sql}) + + return qs + + +def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"): + """Attach is_watcher boolean to each object of the queryset. + + :param user: A users.User object model + :param queryset: A Django queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM notifications_watched + WHERE notifications_watched.content_type_id = {type_id} + AND notifications_watched.object_id = {tbl}.id + AND notifications_watched.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + qs = queryset.extra(select={as_field: sql}) + return qs + + +def attach_total_watchers_to_queryset(queryset, as_field="total_watchers"): + """Attach total_watchers boolean to each object of the queryset. + + :param user: A users.User object model + :param queryset: A Django queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = ("""SELECT count(*) + FROM notifications_watched + WHERE notifications_watched.content_type_id = {type_id} + AND notifications_watched.object_id = {tbl}.id""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table) + qs = queryset.extra(select={as_field: sql}) + return qs + + +def attach_project_is_watcher_to_queryset(queryset, user, as_field="is_watcher"): + """Attach is_watcher boolean to each object of the projects queryset. + + :param user: A users.User object model + :param queryset: A Django projects queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.project_id = {tbl}.id + AND notifications_notifypolicy.user_id = {user_id} + AND notifications_notifypolicy.notify_level != {ignore_notify_level}) > 0 + + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(tbl=model._meta.db_table, user_id=user.id, ignore_notify_level=NotifyLevel.none) + qs = queryset.extra(select={as_field: sql}) + return qs + + +def attach_project_total_watchers_attrs_to_queryset(queryset, as_field="total_watchers"): + """Attach watching user ids to each project of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the watchers as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + + sql = ("""SELECT count(user_id) + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.project_id = {tbl}.id + AND notifications_notifypolicy.notify_level != {ignore_notify_level}""") + sql = sql.format(tbl=model._meta.db_table, ignore_notify_level=NotifyLevel.none) + qs = queryset.extra(select={as_field: sql}) + + return qs + + +def attach_notify_level_to_project_queryset(queryset, user): + """ + Function that attach "notify_level" attribute on each queryset + result for query notification level of current user for each + project in the most efficient way. + + :param queryset: A Django queryset object. + :param user: A User model object. + + :return: Queryset object with the additional `as_field` field. + """ + user_id = getattr(user, "id", None) or "NULL" + default_level = NotifyLevel.involved + + sql = strip_lines(""" + COALESCE((SELECT notifications_notifypolicy.notify_level + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.project_id = projects_project.id + AND notifications_notifypolicy.user_id = {user_id}), + {default_level}) + """) + sql = sql.format(user_id=user_id, default_level=default_level) + return queryset.extra(select={"notify_level": sql}) diff --git a/taiga/projects/notifications/validators.py b/taiga/projects/notifications/validators.py index 38c5750b..1330a09a 100644 --- a/taiga/projects/notifications/validators.py +++ b/taiga/projects/notifications/validators.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -21,7 +21,7 @@ from taiga.base.api import serializers class WatchersValidator: def validate_watchers(self, attrs, source): - users = attrs[source] + users = attrs.get(source, []) # Try obtain a valid project if self.object is None and "project" in attrs: @@ -39,7 +39,9 @@ class WatchersValidator: # Check if incoming watchers are contained # in project members list - result = set(users).difference(set(project.members.all())) + member_ids = project.members.values_list("id", flat=True) + existing_watcher_ids = project.get_watchers().values_list("id", flat=True) + result = set(users).difference(member_ids).difference(existing_watcher_ids) if result: raise serializers.ValidationError(_("Watchers contains invalid users")) diff --git a/taiga/projects/occ/__init__.py b/taiga/projects/occ/__init__.py index 34a54708..a25f77db 100644 --- a/taiga/projects/occ/__init__.py +++ b/taiga/projects/occ/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/occ/mixins.py b/taiga/projects/occ/mixins.py index 777a59b4..b473eb2b 100644 --- a/taiga/projects/occ/mixins.py +++ b/taiga/projects/occ/mixins.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 6f7afc66..0925ce8a 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -54,20 +54,34 @@ class ProjectPermission(TaigaResourcePermission): list_perms = AllowAny() 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() - star_perms = IsAuthenticated() - unstar_perms = IsAuthenticated() - issues_stats_perms = HasProjectPerm('view_project') - issues_filters_data_perms = HasProjectPerm('view_project') tags_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project') - fans_perms = HasProjectPerm('view_project') + like_perms = IsAuthenticated() & HasProjectPerm('view_project') + unlike_perms = IsAuthenticated() & HasProjectPerm('view_project') + watch_perms = IsAuthenticated() & HasProjectPerm('view_project') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project') create_template_perms = IsSuperUser() leave_perms = CanLeaveProject() +class ProjectFansPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_project') + list_perms = HasProjectPerm('view_project') + + +class ProjectWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_project') + list_perms = HasProjectPerm('view_project') + + class MembershipPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') create_perms = IsProjectOwner() diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 97b41de4..4b8027cb 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -59,4 +59,21 @@ class ResolverViewSet(viewsets.ViewSet): result["wikipage"] = get_object_or_404(project.wiki_pages.all(), slug=data["wikipage"]).pk + if data["ref"]: + ref_found = False # No need to continue once one ref is found + if user_has_perm(request.user, "view_us", project): + us = project.user_stories.filter(ref=data["ref"]).first() + if us: + result["us"] = us.pk + ref_found = True + if ref_found is False and user_has_perm(request.user, "view_tasks", project): + task = project.tasks.filter(ref=data["ref"]).first() + if task: + result["task"] = task.pk + ref_found = True + if ref_found is False and user_has_perm(request.user, "view_issues", project): + issue = project.issues.filter(ref=data["ref"]).first() + if issue: + result["issue"] = issue.pk + return response.Ok(result) diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py index 8832034c..6e141c8d 100644 --- a/taiga/projects/references/models.py +++ b/taiga/projects/references/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -80,19 +80,31 @@ def delete_sequence(sender, instance, **kwargs): seq.delete(seqname) -def attach_sequence(sender, instance, created, **kwargs): - if created and not instance._importing: - # Create a reference object. This operation should be - # used in transaction context, otherwise it can - # create a lot of phantom reference objects. - refval, _ = make_reference(instance, instance.project) +def store_previous_project(sender, instance, **kwargs): + try: + prev_instance = sender.objects.get(pk=instance.pk) + instance.prev_project = prev_instance.project + except sender.DoesNotExist: + instance.prev_project = None - # Additionally, attach sequence number to instance as ref - instance.ref = refval - instance.save(update_fields=['ref']) + +def attach_sequence(sender, instance, created, **kwargs): + if not instance._importing: + if created or instance.prev_project != instance.project: + # Create a reference object. This operation should be + # used in transaction context, otherwise it can + # create a lot of phantom reference objects. + refval, _ = make_reference(instance, instance.project) + + # Additionally, attach sequence number to instance as ref + instance.ref = refval + instance.save(update_fields=['ref']) models.signals.post_save.connect(create_sequence, sender=Project, dispatch_uid="refproj") +models.signals.pre_save.connect(store_previous_project, sender=UserStory, dispatch_uid="refus") +models.signals.pre_save.connect(store_previous_project, sender=Issue, dispatch_uid="refissue") +models.signals.pre_save.connect(store_previous_project, sender=Task, dispatch_uid="reftask") models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid="refus") models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue") models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask") diff --git a/taiga/projects/references/permissions.py b/taiga/projects/references/permissions.py index 251e0f88..aa818c49 100644 --- a/taiga/projects/references/permissions.py +++ b/taiga/projects/references/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/references/sequences.py b/taiga/projects/references/sequences.py index 6c90abaa..ca6a4f62 100644 --- a/taiga/projects/references/sequences.py +++ b/taiga/projects/references/sequences.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/references/serializers.py b/taiga/projects/references/serializers.py index 6fb27432..4755a897 100644 --- a/taiga/projects/references/serializers.py +++ b/taiga/projects/references/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,4 +23,16 @@ class ResolverSerializer(serializers.Serializer): us = serializers.IntegerField(required=False) task = serializers.IntegerField(required=False) issue = serializers.IntegerField(required=False) + ref = serializers.IntegerField(required=False) wikipage = serializers.CharField(max_length=512, required=False) + + def validate(self, attrs): + if "ref" in attrs: + if "us" in attrs: + raise serializers.ValidationError("'us' param is incompatible with 'ref' in the same request") + if "task" in attrs: + raise serializers.ValidationError("'task' param is incompatible with 'ref' in the same request") + if "issue" in attrs: + raise serializers.ValidationError("'issue' param is incompatible with 'ref' in the same request") + + return attrs diff --git a/taiga/projects/references/services.py b/taiga/projects/references/services.py index c40ca311..203cd4e2 100644 --- a/taiga/projects/references/services.py +++ b/taiga/projects/references/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 5aa7c019..21347615 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,19 +27,23 @@ from taiga.base.fields import TagsColorsField from taiga.users.services import get_photo_or_gravatar_url from taiga.users.serializers import UserSerializer +from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import ProjectRoleSerializer from taiga.users.validators import RoleExistsValidator from taiga.permissions.service import get_user_project_permissions from taiga.permissions.service import is_project_owner +from taiga.projects.notifications import models as notify_models + from . import models from . import services +from .notifications.mixins import WatchedResourceModelSerializer from .validators import ProjectExistsValidator from .custom_attributes.serializers import UserStoryCustomAttributeSerializer from .custom_attributes.serializers import TaskCustomAttributeSerializer from .custom_attributes.serializers import IssueCustomAttributeSerializer - +from .likes.mixins.serializers import FanResourceSerializerMixin ###################################################### ## Custom values for selectors @@ -197,7 +201,7 @@ class MembershipSerializer(serializers.ModelSerializer): photo = serializers.SerializerMethodField("get_photo") project_name = serializers.SerializerMethodField("get_project_name") project_slug = serializers.SerializerMethodField("get_project_slug") - invited_by = UserSerializer(read_only=True) + invited_by = UserBasicInfoSerializer(read_only=True) class Meta: model = models.Membership @@ -271,21 +275,6 @@ class MembershipAdminSerializer(MembershipSerializer): exclude = ("token",) -class ProjectMembershipSerializer(serializers.ModelSerializer): - role_name = serializers.CharField(source='role.name', required=False, i18n=True) - full_name = serializers.CharField(source='user.get_full_name', required=False) - username = serializers.CharField(source='user.username', required=False) - color = serializers.CharField(source='user.color', required=False) - is_active = serializers.BooleanField(source='user.is_active', required=False) - photo = serializers.SerializerMethodField("get_photo") - - class Meta: - model = models.Membership - - def get_photo(self, project): - return get_photo_or_gravatar_url(project.user) - - class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer): email = serializers.EmailField() role_id = serializers.IntegerField() @@ -297,19 +286,37 @@ class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer): invitation_extra_text = serializers.CharField(required=False, max_length=255) +class ProjectMemberSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(source="user.id", read_only=True) + username = serializers.CharField(source='user.username', read_only=True) + full_name = serializers.CharField(source='user.full_name', read_only=True) + full_name_display = serializers.CharField(source='user.get_full_name', read_only=True) + color = serializers.CharField(source='user.color', read_only=True) + photo = serializers.SerializerMethodField("get_photo") + is_active = serializers.BooleanField(source='user.is_active', read_only=True) + role_name = serializers.CharField(source='role.name', read_only=True, i18n=True) + + class Meta: + model = models.Membership + exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text", "user_order") + + def get_photo(self, membership): + return get_photo_or_gravatar_url(membership.user) + + ###################################################### ## Projects ###################################################### -class ProjectSerializer(serializers.ModelSerializer): +class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer): tags = TagsField(default=[], required=False) anon_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False) - stars = serializers.SerializerMethodField("get_stars_number") my_permissions = serializers.SerializerMethodField("get_my_permissions") i_am_owner = serializers.SerializerMethodField("get_i_am_owner") tags_colors = TagsColorsField(required=False) total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones") + notify_level = serializers.SerializerMethodField("get_notify_level") class Meta: model = models.Project @@ -317,10 +324,6 @@ class ProjectSerializer(serializers.ModelSerializer): exclude = ("last_us_ref", "last_task_ref", "last_issue_ref", "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid") - def get_stars_number(self, obj): - # The "stars_count" attribute is attached in the get_queryset of the viewset. - return getattr(obj, "stars_count", 0) - def get_my_permissions(self, obj): if "request" in self.context: return get_user_project_permissions(self.context["request"].user, obj) @@ -334,23 +337,16 @@ class ProjectSerializer(serializers.ModelSerializer): def get_total_closed_milestones(self, obj): return obj.milestones.filter(closed=True).count() - def validate_total_milestones(self, attrs, source): - """ - Check that total_milestones is not null, it's an optional parameter but - not nullable in the model. - """ - value = attrs[source] - if value is None: - raise serializers.ValidationError(_("Total milestones must be major or equal to zero")) - return attrs + def get_notify_level(self, obj): + return getattr(obj, "notify_level", None) class ProjectDetailSerializer(ProjectSerializer): - roles = serializers.SerializerMethodField("get_roles") - memberships = serializers.SerializerMethodField("get_memberships") us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories points = PointsSerializer(many=True, required=False) + task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks + issue_statuses = IssueStatusSerializer(many=True, required=False) issue_types = IssueTypeSerializer(many=True, required=False) priorities = PrioritySerializer(many=True, required=False) # Issues @@ -362,23 +358,18 @@ class ProjectDetailSerializer(ProjectSerializer): many=True, required=False) issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes", many=True, required=False) - users = serializers.SerializerMethodField("get_users") - def get_memberships(self, obj): + roles = ProjectRoleSerializer(source="roles", many=True, read_only=True) + members = serializers.SerializerMethodField(method_name="get_members") + + def get_members(self, obj): qs = obj.memberships.filter(user__isnull=False) qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"}) qs = qs.order_by("complete_user_name") qs = qs.select_related("role", "user") - serializer = ProjectMembershipSerializer(qs, many=True) + serializer = ProjectMemberSerializer(qs, many=True) return serializer.data - def get_roles(self, obj): - serializer = ProjectRoleSerializer(obj.roles.all(), many=True) - return serializer.data - - def get_users(self, obj): - return UserSerializer(obj.members.all(), many=True).data - class ProjectDetailAdminSerializer(ProjectDetailSerializer): class Meta: @@ -388,10 +379,10 @@ class ProjectDetailAdminSerializer(ProjectDetailSerializer): ###################################################### -## Starred +## Liked ###################################################### -class StarredSerializer(serializers.ModelSerializer): +class LikedSerializer(serializers.ModelSerializer): class Meta: model = models.Project fields = ['id', 'name', 'slug'] diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index 12b4276c..d63cbe79 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,7 +27,6 @@ from .bulk_update_order import bulk_update_points_order from .bulk_update_order import bulk_update_userstory_status_order from .filters import get_all_tags -from .filters import get_issues_filters_data from .stats import get_stats_for_project_issues from .stats import get_stats_for_project diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py index 40499346..83b38a90 100644 --- a/taiga/projects/services/bulk_update_order.py +++ b/taiga/projects/services/bulk_update_order.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/services/filters.py b/taiga/projects/services/filters.py index e8c2786c..e9c3f28e 100644 --- a/taiga/projects/services/filters.py +++ b/taiga/projects/services/filters.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -50,113 +50,6 @@ def _get_issues_tags(project): return result -def _get_issues_tags_with_count(project): - extra_sql = ("select unnest(tags) as tagname, count(unnest(tags)) " - "from issues_issue where project_id = %s " - "group by unnest(tags) " - "order by tagname asc") - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_statuses(project): - extra_sql = ("select status_id, count(status_id) from issues_issue " - "where project_id = %s group by status_id;") - - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and status_id = m.id) - from projects_issuestatus as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_priorities(project): - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and priority_id = m.id) - from projects_priority as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - -def _get_issues_types(project): - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and type_id = m.id) - from projects_issuetype as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_severities(project): - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and severity_id = m.id) - from projects_severity as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_assigned_to(project): - extra_sql = """ - select null, (select count(*) from issues_issue - where project_id = %s and assigned_to_id is null) - UNION select user_id, (select count(*) from issues_issue - where project_id = pm.project_id and assigned_to_id = pm.user_id) - from projects_membership as pm - where project_id = %s and pm.user_id is not null; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id, project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_owners(project): - extra_sql = """ - select user_id, (select count(*) from issues_issue - where project_id = pm.project_id and owner_id = pm.user_id) - from projects_membership as pm - where project_id = %s and pm.user_id is not null; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - # Public api def get_all_tags(project): @@ -170,23 +63,3 @@ def get_all_tags(project): result.update(_get_stories_tags(project)) result.update(_get_tasks_tags(project)) return sorted(result) - - -def get_issues_filters_data(project): - """ - Given a project, return a simple data structure - of all possible filters for issues. - """ - - data = { - "types": _get_issues_types(project), - "statuses": _get_issues_statuses(project), - "priorities": _get_issues_priorities(project), - "severities": _get_issues_severities(project), - "assigned_to": _get_issues_assigned_to(project), - "created_by": _get_issues_owners(project), - "owners": _get_issues_owners(project), - "tags": _get_issues_tags_with_count(project), - } - - return data diff --git a/taiga/projects/services/invitations.py b/taiga/projects/services/invitations.py index 4196612c..50e1e01e 100644 --- a/taiga/projects/services/invitations.py +++ b/taiga/projects/services/invitations.py @@ -1,17 +1,16 @@ from django.apps import apps from django.conf import settings -from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail +from taiga.base.mails import mail_builder def send_invitation(invitation): """Send an invitation email""" - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) if invitation.user: - template = mbuilder.membership_notification + template = mail_builder.membership_notification email = template(invitation.user, {"membership": invitation}) else: - template = mbuilder.membership_invitation + template = mail_builder.membership_invitation email = template(invitation.email, {"membership": invitation}) email.send() diff --git a/taiga/projects/services/modules_config.py b/taiga/projects/services/modules_config.py index 4b1cbae8..c0b92d77 100644 --- a/taiga/projects/services/modules_config.py +++ b/taiga/projects/services/modules_config.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py index b7586cb5..2b4ec4a1 100644 --- a/taiga/projects/services/stats.py +++ b/taiga/projects/services/stats.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -21,8 +21,17 @@ import datetime import copy from taiga.projects.history.models import HistoryEntry +from taiga.projects.userstories.models import RolePoints +def _get_total_story_points(project): + return (project.total_story_points if project.total_story_points not in [None, 0] else + sum(project.calculated_points["defined"].values())) + +def _get_total_milestones(project): + return (project.total_milestones if project.total_milestones not in [None, 0] else + project.milestones.count()) + def _get_milestones_stats_for_backlog(project): """ Get collection of stats for each millestone of project. @@ -33,8 +42,12 @@ def _get_milestones_stats_for_backlog(project): current_client_increment = 0 optimal_points_per_sprint = 0 - if project.total_story_points and project.total_milestones: - optimal_points_per_sprint = project.total_story_points / project.total_milestones + + total_story_points = _get_total_story_points(project) + total_milestones = _get_total_milestones(project) + + if total_story_points and total_milestones: + optimal_points_per_sprint = total_story_points / total_milestones future_team_increment = sum(project.future_team_increment.values()) future_client_increment = sum(project.future_client_increment.values()) @@ -50,11 +63,11 @@ def _get_milestones_stats_for_backlog(project): team_increment = 0 client_increment = 0 - for current_milestone in range(0, max(milestones_count, project.total_milestones)): - optimal_points = (project.total_story_points - + for current_milestone in range(0, max(milestones_count, total_milestones)): + optimal_points = (total_story_points - (optimal_points_per_sprint * current_milestone)) - evolution = (project.total_story_points - current_evolution + evolution = (total_story_points - current_evolution if current_evolution is not None else None) if current_milestone < milestones_count: @@ -83,8 +96,8 @@ def _get_milestones_stats_for_backlog(project): } optimal_points -= optimal_points_per_sprint - evolution = (project.total_story_points - current_evolution - if current_evolution is not None and project.total_story_points else None) + evolution = (total_story_points - current_evolution + if current_evolution is not None and total_story_points else None) yield { 'name': _('Project End'), 'optimal': optimal_points, @@ -104,6 +117,7 @@ def _count_status_object(status_obj, counting_storage): counting_storage[status_obj.id]['id'] = status_obj.id counting_storage[status_obj.id]['color'] = status_obj.color + def _count_owned_object(user_obj, counting_storage): if user_obj: if user_obj.id in counting_storage: @@ -126,6 +140,7 @@ def _count_owned_object(user_obj, counting_storage): counting_storage[0]['id'] = 0 counting_storage[0]['color'] = 'black' + def get_stats_for_project_issues(project): project_issues_stats = { 'total_issues': 0, @@ -210,7 +225,12 @@ def get_stats_for_project(project): get(id=project.id) points = project.calculated_points - closed_points = sum(points["closed"].values()) + + closed_points = sum(RolePoints.objects.filter(user_story__project=project).filter( + Q(user_story__milestone__closed=True) | + Q(user_story__milestone__isnull=True) + ).exclude(points__value__isnull=True).values_list("points__value", flat=True)) + closed_milestones = project.milestones.filter(closed=True).count() speed = 0 if closed_milestones != 0: @@ -283,6 +303,7 @@ def _get_closed_tasks_per_member_stats(project): closed_tasks = {p["assigned_to"]: p["count"] for p in closed_tasks} return closed_tasks + def get_member_stats_for_project(project): base_counters = {id: 0 for id in project.members.values_list("id", flat=True)} closed_bugs = base_counters.copy() diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py index 52ac61ff..14d1dbce 100644 --- a/taiga/projects/services/tags_colors.py +++ b/taiga/projects/services/tags_colors.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py index 61dd0709..b4652a8b 100644 --- a/taiga/projects/signals.py +++ b/taiga/projects/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -45,24 +45,6 @@ def membership_post_delete(sender, instance, using, **kwargs): instance.project.update_role_points() -def update_watchers_on_membership_post_delete(sender, instance, using, **kwargs): - models = [apps.get_model("userstories", "UserStory"), - apps.get_model("tasks", "Task"), - apps.get_model("issues", "Issue")] - - # `user_id` is used beacuse in some momments - # instance.user can contain pointer to now - # removed object from a database. - for model in models: - #filter(project=instance.project) - filter = { - "user_id": instance.user_id, - "%s__project"%(model._meta.model_name): instance.project, - } - - model.watchers.through.objects.filter(**filter).delete() - - def create_notify_policy(sender, instance, using, **kwargs): if instance.user: create_notify_policy_if_not_exists(instance.project, instance.user) @@ -97,3 +79,25 @@ def project_post_save(sender, instance, created, **kwargs): Membership = apps.get_model("projects", "Membership") Membership.objects.create(user=instance.owner, project=instance, role=owner_role, is_owner=True, email=instance.owner.email) + + +def try_to_close_or_open_user_stories_when_edit_us_status(sender, instance, created, **kwargs): + from taiga.projects.userstories import services + + for user_story in instance.user_stories.all(): + if services.calculate_userstory_is_closed(user_story): + services.close_userstory(user_story) + else: + services.open_userstory(user_story) + + +def try_to_close_or_open_user_stories_when_edit_task_status(sender, instance, created, **kwargs): + from taiga.projects.userstories import services + + UserStory = apps.get_model("userstories", "UserStory") + + for user_story in UserStory.objects.filter(tasks__status=instance).distinct(): + if services.calculate_userstory_is_closed(user_story): + services.close_userstory(user_story) + else: + services.open_userstory(user_story) diff --git a/taiga/projects/tasks/__init__.py b/taiga/projects/tasks/__init__.py index 0af24e1d..6e63190e 100644 --- a/taiga/projects/tasks/__init__.py +++ b/taiga/projects/tasks/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/tasks/admin.py b/taiga/projects/tasks/admin.py index 937e70a3..4c451c17 100644 --- a/taiga/projects/tasks/admin.py +++ b/taiga/projects/tasks/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,6 +17,9 @@ from django.contrib import admin from taiga.projects.attachments.admin import AttachmentInline +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline + from . import models @@ -24,7 +27,7 @@ class TaskAdmin(admin.ModelAdmin): list_display = ["project", "milestone", "user_story", "ref", "subject",] list_display_links = ["ref", "subject",] list_filter = ["project"] - # inlines = [AttachmentInline] + inlines = [WatchedInline, VoteInline] def get_object(self, *args, **kwargs): self.obj = super().get_object(*args, **kwargs) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 8dd88dea..d48791e2 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -20,13 +20,14 @@ from taiga.base.api.utils import get_object_or_404 from taiga.base import filters, response from taiga.base import exceptions as exc from taiga.base.decorators import list_route -from taiga.base.api import ModelCrudViewSet -from taiga.projects.models import Project +from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.projects.models import Project, TaskStatus from django.http import HttpResponse -from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.occ import OCCResourceMixin +from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -35,12 +36,14 @@ from . import serializers from . import services -class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): - model = models.Task +class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, + ModelCrudViewSet): + queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) - filter_backends = (filters.CanViewTasksFilterBackend,) + filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter) + retrieve_exclude_filters = (filters.WatchersFilter,) filter_fields = ["user_story", "milestone", "project", "assigned_to", - "status__is_closed", "watchers"] + "status__is_closed"] def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: @@ -51,6 +54,42 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, return serializers.TaskSerializer + def update(self, request, *args, **kwargs): + self.object = self.get_object_or_none() + project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: + try: + new_project = Project.objects.get(pk=project_id) + self.check_permissions(request, "destroy", self.object) + self.check_permissions(request, "create", new_project) + + sprint_id = request.DATA.get('milestone', None) + if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: + request.DATA['milestone'] = None + + us_id = request.DATA.get('user_story', None) + if us_id is not None and new_project.user_stories.filter(pk=us_id).count() == 0: + request.DATA['user_story'] = None + + status_id = request.DATA.get('status', None) + if status_id is not None: + try: + old_status = self.object.project.task_statuses.get(pk=status_id) + new_status = new_project.task_statuses.get(slug=old_status.slug) + request.DATA['status'] = new_status.id + except TaskStatus.DoesNotExist: + request.DATA['status'] = new_project.default_task_status.id + + except Project.DoesNotExist: + return response.BadRequest(_("The project doesn't exist")) + + return super().update(request, *args, **kwargs) + + def get_queryset(self): + qs = super().get_queryset() + qs = self.attach_votes_attrs_to_queryset(qs) + return self.attach_watchers_attrs_to_queryset(qs) + def pre_save(self, obj): if obj.user_story: obj.milestone = obj.user_story.milestone @@ -62,16 +101,16 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, super().pre_conditions_on_save(obj) if obj.milestone and obj.milestone.project != obj.project: - raise exc.WrongArguments(_("You don't have permissions for add/modify this task.")) + raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) if obj.user_story and obj.user_story.project != obj.project: - raise exc.WrongArguments(_("You don't have permissions for add/modify this task.")) + raise exc.WrongArguments(_("You don't have permissions to set this user story to this task.")) if obj.status and obj.status.project != obj.project: - raise exc.WrongArguments(_("You don't have permissions for add/modify this task.")) + raise exc.WrongArguments(_("You don't have permissions to set this status to this task.")) if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone: - raise exc.WrongArguments(_("You don't have permissions for add/modify this task.")) + raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) @list_route(methods=["GET"]) def by_ref(self, request): @@ -133,3 +172,13 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, @list_route(methods=["POST"]) def bulk_update_us_order(self, request, **kwargs): return self._bulk_update_order("us_order", request, **kwargs) + + +class TaskVotersViewSet(VotersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.TaskVotersPermission,) + resource_model = models.Task + + +class TaskWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.TaskWatchersPermission,) + resource_model = models.Task diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 445ca63b..d495c964 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,19 +23,6 @@ from taiga.projects.custom_attributes import signals as custom_attributes_handle from . import signals as handlers def connect_tasks_signals(): - # Cached prev object version - signals.pre_save.connect(handlers.cached_prev_task, - sender=apps.get_model("tasks", "Task"), - dispatch_uid="cached_prev_task") - - # Open/Close US and Milestone - signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_task, - sender=apps.get_model("tasks", "Task"), - dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task") - signals.post_delete.connect(handlers.try_to_close_or_open_us_and_milestone_when_delete_task, - sender=apps.get_model("tasks", "Task"), - dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") - # Tags signals.pre_save.connect(generic_handlers.tags_normalization, sender=apps.get_model("tasks", "Task"), @@ -47,6 +34,18 @@ def connect_tasks_signals(): sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_delete_tagglabe_item_task") +def connect_tasks_close_or_open_us_and_milestone_signals(): + # Cached prev object version + signals.pre_save.connect(handlers.cached_prev_task, + sender=apps.get_model("tasks", "Task"), + dispatch_uid="cached_prev_task") + # Open/Close US and Milestone + signals.post_save.connect(handlers.try_to_close_or_open_us_and_milestone_when_create_or_edit_task, + sender=apps.get_model("tasks", "Task"), + dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task") + signals.post_delete.connect(handlers.try_to_close_or_open_us_and_milestone_when_delete_task, + sender=apps.get_model("tasks", "Task"), + dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") def connect_tasks_custom_attributes_signals(): signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task, @@ -56,24 +55,29 @@ def connect_tasks_custom_attributes_signals(): def connect_all_tasks_signals(): connect_tasks_signals() + connect_tasks_close_or_open_us_and_milestone_signals() connect_tasks_custom_attributes_signals() def disconnect_tasks_signals(): - signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="cached_prev_task") - signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task") - signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization") signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item") signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_delete_tagglabe_item") +def disconnect_tasks_close_or_open_us_and_milestone_signals(): + signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="cached_prev_task") + signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task") + signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") + + def disconnect_tasks_custom_attributes_signals(): signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="create_custom_attribute_value_when_create_task") def disconnect_all_tasks_signals(): disconnect_tasks_signals() + disconnect_tasks_close_or_open_us_and_milestone_signals() disconnect_tasks_custom_attributes_signals() diff --git a/taiga/projects/tasks/migrations/0004_auto_20141210_1107.py b/taiga/projects/tasks/migrations/0004_auto_20141210_1107.py index b4c093f3..33d7c053 100644 --- a/taiga/projects/tasks/migrations/0004_auto_20141210_1107.py +++ b/taiga/projects/tasks/migrations/0004_auto_20141210_1107.py @@ -21,7 +21,6 @@ def _fix_tags_model(tags_model): def fix_tags(apps, schema_editor): - print("Fixing user task tags") _fix_tags_model(Task) diff --git a/taiga/projects/tasks/migrations/0008_remove_task_watchers.py b/taiga/projects/tasks/migrations/0008_remove_task_watchers.py new file mode 100644 index 00000000..639e12b1 --- /dev/null +++ b/taiga/projects/tasks/migrations/0008_remove_task_watchers.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import connection +from django.db import models, migrations +from django.contrib.contenttypes.models import ContentType +from taiga.base.utils.contenttypes import update_all_contenttypes + +def create_notifications(apps, schema_editor): + update_all_contenttypes(verbosity=0) + sql=""" +INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id) +SELECT task_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id +FROM tasks_task_watchers INNER JOIN tasks_task ON tasks_task_watchers.task_id = tasks_task.id""".format(content_type_id=ContentType.objects.get(model='task').id) + cursor = connection.cursor() + cursor.execute(sql) + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_watched'), + ('tasks', '0007_auto_20150629_1556'), + ] + + operations = [ + migrations.RunPython(create_notifications), + migrations.RemoveField( + model_name='task', + name='watchers', + ), + ] diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 37176fab..c43869cb 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 2c1fd7b0..7a12cd13 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,7 +15,8 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectOwner, AllowAny, IsSuperUser) + IsAuthenticated, IsProjectOwner, AllowAny, + IsSuperUser) class TaskPermission(TaigaResourcePermission): @@ -30,3 +31,21 @@ class TaskPermission(TaigaResourcePermission): csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_task') bulk_update_order_perms = HasProjectPerm('modify_task') + upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks') + downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks') + watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_tasks') + + +class TaskVotersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_tasks') + list_perms = HasProjectPerm('view_tasks') + + +class TaskWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_tasks') + list_perms = HasProjectPerm('view_tasks') diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index d6241ca4..deb9af68 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,12 +27,15 @@ from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.tasks.validators import TaskExistsValidator from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicTaskStatusSerializerSerializer -from taiga.users.serializers import BasicInfoSerializer as UserBasicInfoSerializer +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin + +from taiga.users.serializers import UserBasicInfoSerializer from . import models -class TaskSerializer(WatchersValidator, serializers.ModelSerializer): +class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): tags = TagsField(required=False, default=[]) external_reference = PgArrayField(required=False) comment = serializers.SerializerMethodField("get_comment") @@ -42,6 +45,7 @@ class TaskSerializer(WatchersValidator, serializers.ModelSerializer): is_closed = serializers.SerializerMethodField("get_is_closed") status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True) assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) + owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) class Meta: model = models.Task diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 61225aff..46cbedfd 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,6 +23,7 @@ from taiga.projects.tasks.apps import ( connect_tasks_signals, disconnect_tasks_signals) from taiga.events import events +from taiga.projects.votes import services as votes_services from . import models @@ -95,7 +96,8 @@ def tasks_to_csv(project, queryset): fieldnames = ["ref", "subject", "description", "user_story", "milestone", "owner", "owner_full_name", "assigned_to", "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order", - "taskboard_order", "attachments", "external_reference", "tags"] + "taskboard_order", "attachments", "external_reference", "tags", + "watchers", "voters"] for custom_attr in project.taskcustomattributes.all(): fieldnames.append(custom_attr.name) @@ -120,6 +122,8 @@ def tasks_to_csv(project, queryset): "attachments": task.attachments.count(), "external_reference": task.external_reference, "tags": ",".join(task.tags or []), + "watchers": [u.id for u in task.get_watchers()], + "voters": votes_services.get_voters(task).count(), } for custom_attr in project.taskcustomattributes.all(): value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) diff --git a/taiga/projects/tasks/signals.py b/taiga/projects/tasks/signals.py index bdc622e9..07a7738d 100644 --- a/taiga/projects/tasks/signals.py +++ b/taiga/projects/tasks/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/translations.py b/taiga/projects/translations.py index 74b4b53e..b0b32671 100644 --- a/taiga/projects/translations.py +++ b/taiga/projects/translations.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/userstories/__init__.py b/taiga/projects/userstories/__init__.py index 572f9d9a..13d664b6 100644 --- a/taiga/projects/userstories/__init__.py +++ b/taiga/projects/userstories/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/userstories/admin.py b/taiga/projects/userstories/admin.py index cd23dc35..2887e836 100644 --- a/taiga/projects/userstories/admin.py +++ b/taiga/projects/userstories/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,6 +17,8 @@ from django.contrib import admin from taiga.projects.attachments.admin import AttachmentInline +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline from . import models @@ -41,7 +43,7 @@ class UserStoryAdmin(admin.ModelAdmin): list_display = ["project", "milestone", "ref", "subject",] list_display_links = ["ref", "subject",] list_filter = ["project"] - inlines = [RolePointsInline] + inlines = [RolePointsInline, WatchedInline, VoteInline] def get_object(self, *args, **kwargs): self.obj = super().get_object(*args, **kwargs) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index edd09821..66c7b521 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -28,15 +28,15 @@ 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 import ModelCrudViewSet +from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.utils import get_object_or_404 -from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.occ import OCCResourceMixin - from taiga.projects.models import Project, UserStoryStatus from taiga.projects.history.services import take_snapshot +from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models from . import permissions @@ -44,18 +44,36 @@ from . import serializers from . import services -class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): - model = models.UserStory +class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, + ModelCrudViewSet): + queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) + filter_backends = (filters.CanViewUsFilterBackend, + filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.WatchersFilter, + filters.QFilter, + filters.OrderByFilterMixin) + retrieve_exclude_filters = (filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.WatchersFilter) + filter_fields = ["project", + "milestone", + "milestone__isnull", + "is_closed", + "status__is_archived", + "status__is_closed"] + order_by_fields = ["backlog_order", + "sprint_order", + "kanban_order", + "total_voters"] - filter_backends = (filters.StatusFilter, filters.CanViewUsFilterBackend, filters.TagsFilter, - filters.QFilter, filters.OrderByFilterMixin) - - retrieve_exclude_filters = (filters.StatusFilter, filters.TagsFilter,) - filter_fields = ["project", "milestone", "milestone__isnull", - "is_archived", "status__is_archived", "assigned_to", - "status__is_closed", "watchers", "is_closed"] - order_by_fields = ["backlog_order", "sprint_order", "kanban_order"] + # Specific filter used for filtering neighbor user stories + _neighbor_tags_filter = filters.TagsFilter('neighbor_tags') def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: @@ -66,17 +84,41 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi return serializers.UserStorySerializer - # Specific filter used for filtering neighbor user stories - _neighbor_tags_filter = filters.TagsFilter('neighbor_tags') + def update(self, request, *args, **kwargs): + self.object = self.get_object_or_none() + project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: + try: + new_project = Project.objects.get(pk=project_id) + self.check_permissions(request, "destroy", self.object) + self.check_permissions(request, "create", new_project) + + sprint_id = request.DATA.get('milestone', None) + if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: + request.DATA['milestone'] = None + + status_id = request.DATA.get('status', None) + if status_id is not None: + try: + old_status = self.object.project.us_statuses.get(pk=status_id) + new_status = new_project.us_statuses.get(slug=old_status.slug) + request.DATA['status'] = new_status.id + except UserStoryStatus.DoesNotExist: + request.DATA['status'] = new_project.default_us_status.id + except Project.DoesNotExist: + return response.BadRequest(_("The project doesn't exist")) + + return super().update(request, *args, **kwargs) + def get_queryset(self): - qs = self.model.objects.all() + qs = super().get_queryset() qs = qs.prefetch_related("role_points", "role_points__points", - "role_points__role", - "watchers") + "role_points__role") qs = qs.select_related("milestone", "project") - return qs + qs = self.attach_votes_attrs_to_queryset(qs) + return self.attach_watchers_attrs_to_queryset(qs) def pre_save(self, obj): # This is very ugly hack, but having @@ -107,6 +149,37 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi super().post_save(obj, created) + def pre_conditions_on_save(self, obj): + super().pre_conditions_on_save(obj) + + if obj.milestone and obj.milestone.project != obj.project: + raise exc.PermissionDenied(_("You don't have permissions to set this sprint " + "to this user story.")) + + if obj.status and obj.status.project != obj.project: + raise exc.PermissionDenied(_("You don't have permissions to set this status " + "to this user story.")) + + @list_route(methods=["GET"]) + def filters_data(self, request, *args, **kwargs): + project_id = request.QUERY_PARAMS.get("project", None) + project = get_object_or_404(Project, id=project_id) + + filter_backends = self.get_filter_backends() + statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) + assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) + owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) + tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter) + + queryset = self.get_queryset() + querysets = { + "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), + "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), + "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), + "tags": self.filter_queryset(queryset) + } + return response.Ok(services.get_userstories_filters_data(project, querysets)) + @list_route(methods=["GET"]) def by_ref(self, request): ref = request.QUERY_PARAMS.get("ref", None) @@ -178,8 +251,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue: self.object.generated_from_issue.save() - comment = _("Generating the user story [US #{ref} - " - "{subject}](:us:{ref} \"US #{ref} - {subject}\")") + comment = _("Generating the user story #{ref} - {subject}") comment = comment.format(ref=self.object.ref, subject=self.object.subject) history = take_snapshot(self.object.generated_from_issue, comment=comment, @@ -188,3 +260,12 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi self.send_notifications(self.object.generated_from_issue, history) return response + +class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.UserStoryVotersPermission,) + resource_model = models.UserStory + + +class UserStoryWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.UserStoryWatchersPermission,) + resource_model = models.UserStory diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index 948e7c08..240e0375 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/userstories/migrations/0008_auto_20141210_1107.py b/taiga/projects/userstories/migrations/0008_auto_20141210_1107.py index 8c4b6c8d..36e032fd 100644 --- a/taiga/projects/userstories/migrations/0008_auto_20141210_1107.py +++ b/taiga/projects/userstories/migrations/0008_auto_20141210_1107.py @@ -21,7 +21,6 @@ def _fix_tags_model(tags_model): def fix_tags(apps, schema_editor): - print("Fixing user story tags") _fix_tags_model(UserStory) diff --git a/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py b/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py new file mode 100644 index 00000000..e3b94599 --- /dev/null +++ b/taiga/projects/userstories/migrations/0010_remove_userstory_watchers.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import connection +from django.db import models, migrations +from django.contrib.contenttypes.models import ContentType +from taiga.base.utils.contenttypes import update_all_contenttypes + +def create_notifications(apps, schema_editor): + update_all_contenttypes(verbosity=0) + sql=""" +INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id) +SELECT userstory_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id +FROM userstories_userstory_watchers INNER JOIN userstories_userstory ON userstories_userstory_watchers.userstory_id = userstories_userstory.id""".format(content_type_id=ContentType.objects.get(model='userstory').id) + cursor = connection.cursor() + cursor.execute(sql) + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_watched'), + ('userstories', '0009_remove_userstory_is_archived'), + ] + + operations = [ + migrations.RunPython(create_notifications), + migrations.RemoveField( + model_name='userstory', + name='watchers', + ), + ] diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 5424cb7f..2b29a275 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index c832277d..95a8e622 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -26,6 +26,25 @@ class UserStoryPermission(TaigaResourcePermission): partial_update_perms = HasProjectPerm('modify_us') destroy_perms = HasProjectPerm('delete_us') list_perms = AllowAny() + filters_data_perms = AllowAny() csv_perms = AllowAny() bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_update_order_perms = HasProjectPerm('modify_us') + upvote_perms = IsAuthenticated() & HasProjectPerm('view_us') + downvote_perms = IsAuthenticated() & HasProjectPerm('view_us') + watch_perms = IsAuthenticated() & HasProjectPerm('view_us') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_us') + + +class UserStoryVotersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_us') + list_perms = HasProjectPerm('view_us') + + +class UserStoryWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_us') + list_perms = HasProjectPerm('view_us') diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index b26b2dc1..c3f93d67 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -27,7 +27,10 @@ from taiga.projects.validators import UserStoryStatusExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicUserStoryStatusSerializer -from taiga.users.serializers import BasicInfoSerializer as UserBasicInfoSerializer +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin + +from taiga.users.serializers import UserBasicInfoSerializer from . import models @@ -42,7 +45,7 @@ class RolePointsField(serializers.WritableField): return json.loads(obj) -class UserStorySerializer(WatchersValidator, serializers.ModelSerializer): +class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): tags = TagsField(default=[], required=False) external_reference = PgArrayField(required=False) points = RolePointsField(source="role_points", required=False) @@ -55,6 +58,7 @@ class UserStorySerializer(WatchersValidator, serializers.ModelSerializer): description_html = serializers.SerializerMethodField("get_description_html") status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True) assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) + owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) class Meta: model = models.UserStory diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 4577d55c..d1e54394 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,8 +16,13 @@ import csv import io +from collections import OrderedDict +from operator import itemgetter +from contextlib import closing +from django.db import connection from django.utils import timezone +from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot @@ -26,6 +31,7 @@ from taiga.projects.userstories.apps import ( disconnect_userstories_signals) from taiga.events import events +from taiga.projects.votes import services as votes_services from . import models @@ -133,7 +139,8 @@ def userstories_to_csv(project,queryset): "created_date", "modified_date", "finish_date", "client_requirement", "team_requirement", "attachments", "generated_from_issue", "external_reference", "tasks", - "tags"] + "tags", + "watchers", "voters"] for custom_attr in project.userstorycustomattributes.all(): fieldnames.append(custom_attr.name) @@ -165,6 +172,8 @@ def userstories_to_csv(project,queryset): "external_reference": us.external_reference, "tasks": ",".join([str(task.ref) for task in us.tasks.all()]), "tags": ",".join(us.tags or []), + "watchers": [u.id for u in us.get_watchers()], + "voters": votes_services.get_voters(us).count(), } for role in us.project.roles.filter(computable=True).order_by('name'): @@ -181,3 +190,144 @@ def userstories_to_csv(project,queryset): writer.writerow(row) return csv_data + + +def _get_userstories_statuses(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_userstorystatus"."id", + "projects_userstorystatus"."name", + "projects_userstorystatus"."color", + "projects_userstorystatus"."order", + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} AND "userstories_userstory"."status_id" = "projects_userstorystatus"."id") + FROM "projects_userstorystatus" + WHERE "projects_userstorystatus"."project_id" = %s + ORDER BY "projects_userstorystatus"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_userstories_assigned_to(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT NULL, + NULL, + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id" ) + WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL) + UNION SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id" ) + WHERE {where} AND "userstories_userstory"."assigned_to_id" = "projects_membership"."user_id") + FROM "projects_membership" + INNER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + result.append({ + "id": id, + "full_name": full_name or "", + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_userstories_owners(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} AND "userstories_userstory"."owner_id" = "projects_membership"."user_id") + FROM "projects_membership" + RIGHT OUTER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE (("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL) + OR ("users_user"."is_system" IS TRUE)); + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + if count > 0: + result.append({ + "id": id, + "full_name": full_name, + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_userstories_tags(queryset): + tags = [] + for t_list in queryset.values_list("tags", flat=True): + if t_list is None: + continue + tags += list(t_list) + + tags = [{"name":e, "count":tags.count(e)} for e in set(tags)] + + return sorted(tags, key=itemgetter("name")) + + +def get_userstories_filters_data(project, querysets): + """ + Given a project and an userstories queryset, return a simple data structure + of all possible filters for the userstories in the queryset. + """ + data = OrderedDict([ + ("statuses", _get_userstories_statuses(project, querysets["statuses"])), + ("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])), + ("owners", _get_userstories_owners(project, querysets["owners"])), + ("tags", _get_userstories_tags(querysets["tags"])), + ]) + + return data diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py index 8764b60d..ce452992 100644 --- a/taiga/projects/userstories/signals.py +++ b/taiga/projects/userstories/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 3efd7b8f..f8e2440d 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index b6ed0509..11cd9f38 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/votes/admin.py b/taiga/projects/votes/admin.py new file mode 100644 index 00000000..3ab238aa --- /dev/null +++ b/taiga/projects/votes/admin.py @@ -0,0 +1,25 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline + +from . import models + + +class VoteInline(GenericTabularInline): + model = models.Vote + extra = 0 diff --git a/taiga/projects/votes/migrations/0002_auto_20150805_1600.py b/taiga/projects/votes/migrations/0002_auto_20150805_1600.py new file mode 100644 index 00000000..c57f526c --- /dev/null +++ b/taiga/projects/votes/migrations/0002_auto_20150805_1600.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.utils.timezone import utc +from django.conf import settings +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('votes', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='vote', + name='created_date', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2015, 8, 5, 16, 0, 40, 158374, tzinfo=utc), verbose_name='created date'), + preserve_default=False, + ), + migrations.AlterField( + model_name='vote', + name='user', + field=models.ForeignKey(related_name='votes', to=settings.AUTH_USER_MODEL, verbose_name='user'), + preserve_default=True, + ), + migrations.AlterField( + model_name='votes', + name='count', + field=models.PositiveIntegerField(default=0, verbose_name='count'), + preserve_default=True, + ), + ] diff --git a/taiga/projects/votes/mixins/__init__.py b/taiga/projects/votes/mixins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py new file mode 100644 index 00000000..73e1799b --- /dev/null +++ b/taiga/projects/votes/mixins/serializers.py @@ -0,0 +1,30 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api import serializers + + +class VoteResourceSerializerMixin(serializers.ModelSerializer): + is_voter = serializers.SerializerMethodField("get_is_voter") + total_voters = serializers.SerializerMethodField("get_total_voters") + + def get_is_voter(self, obj): + # The "is_voted" attribute is attached in the get_queryset of the viewset. + return getattr(obj, "is_voter", False) or False + + def get_total_voters(self, obj): + # The "total_voters" attribute is attached in the get_queryset of the viewset. + return getattr(obj, "total_voters", 0) or 0 diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py new file mode 100644 index 00000000..aa2100a0 --- /dev/null +++ b/taiga/projects/votes/mixins/viewsets.py @@ -0,0 +1,92 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# 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.exceptions import ObjectDoesNotExist + +from taiga.base import response +from taiga.base.api import viewsets +from taiga.base.api.utils import get_object_or_404 +from taiga.base.decorators import detail_route + +from taiga.projects.votes import serializers +from taiga.projects.votes import services +from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset + + +class VotedResourceMixin: + # Note: Update get_queryset method: + # def get_queryset(self): + # qs = super().get_queryset() + # return self.attach_votes_attrs_to_queryset(qs) + + def attach_votes_attrs_to_queryset(self, queryset): + qs = attach_total_voters_to_queryset(queryset) + + if self.request.user.is_authenticated(): + qs = attach_is_voter_to_queryset(self.request.user, qs) + + return qs + + @detail_route(methods=["POST"]) + def upvote(self, request, pk=None): + obj = self.get_object() + self.check_permissions(request, "upvote", obj) + + services.add_vote(obj, user=request.user) + return response.Ok() + + @detail_route(methods=["POST"]) + def downvote(self, request, pk=None): + obj = self.get_object() + self.check_permissions(request, "downvote", obj) + + services.remove_vote(obj, user=request.user) + return response.Ok() + + +class VotersViewSetMixin: + # Is a ModelListViewSet with two required params: permission_classes and resource_model + serializer_class = serializers.VoterSerializer + list_serializer_class = serializers.VoterSerializer + permission_classes = None + resource_model = None + + def retrieve(self, request, *args, **kwargs): + pk = kwargs.get("pk", None) + resource_id = kwargs.get("resource_id", None) + resource = get_object_or_404(self.resource_model, pk=resource_id) + + self.check_permissions(request, 'retrieve', resource) + + try: + self.object = services.get_voters(resource).get(pk=pk) + except ObjectDoesNotExist: # or User.DoesNotExist + return response.NotFound() + + serializer = self.get_serializer(self.object) + return response.Ok(serializer.data) + + def list(self, request, *args, **kwargs): + resource_id = kwargs.get("resource_id", None) + resource = get_object_or_404(self.resource_model, pk=resource_id) + + self.check_permissions(request, 'list', resource) + + return super().list(request, *args, **kwargs) + + def get_queryset(self): + resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id")) + return services.get_voters(resource) diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py index 5457c3ac..6f5abbe5 100644 --- a/taiga/projects/votes/models.py +++ b/taiga/projects/votes/models.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -16,16 +16,16 @@ # along with this program. If not, see . from django.conf import settings +from django.contrib.contenttypes import generic from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.contrib.contenttypes import generic class Votes(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey("content_type", "object_id") - count = models.PositiveIntegerField(default=0) + count = models.PositiveIntegerField(null=False, blank=False, default=0, verbose_name=_("count")) class Meta: verbose_name = _("Votes") @@ -44,10 +44,12 @@ class Votes(models.Model): class Vote(models.Model): content_type = models.ForeignKey("contenttypes.ContentType") - object_id = models.PositiveIntegerField(null=False) + object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey("content_type", "object_id") user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, - related_name="votes", verbose_name=_("votes")) + related_name="votes", verbose_name=_("user")) + created_date = models.DateTimeField(null=False, blank=False, auto_now_add=True, + verbose_name=_("created date")) class Meta: verbose_name = _("Vote") @@ -61,4 +63,4 @@ class Vote(models.Model): return None def __str__(self): - return self.user + return self.user.get_full_name() diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py index c72ae91e..210c6057 100644 --- a/taiga/projects/votes/serializers.py +++ b/taiga/projects/votes/serializers.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -16,8 +16,10 @@ # along with this program. If not, see . from taiga.base.api import serializers +from taiga.base.fields import TagsField from taiga.users.models import User +from taiga.users.services import get_photo_or_gravatar_url class VoterSerializer(serializers.ModelSerializer): diff --git a/taiga/projects/votes/services.py b/taiga/projects/votes/services.py index ddc1deae..093b685e 100644 --- a/taiga/projects/votes/services.py +++ b/taiga/projects/votes/services.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -35,7 +35,6 @@ def add_vote(obj, user): obj_type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(obj) with atomic(): vote, created = Vote.objects.get_or_create(content_type=obj_type, object_id=obj.id, user=user) - if not created: return diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py index 20e72b6d..dd703bcf 100644 --- a/taiga/projects/votes/utils.py +++ b/taiga/projects/votes/utils.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -18,7 +18,7 @@ from django.apps import apps -def attach_votescount_to_queryset(queryset, as_field="votes_count"): +def attach_total_voters_to_queryset(queryset, as_field="total_voters"): """Attach votes count to each object of the queryset. Because of laziness of vote objects creation, this makes much simpler and more efficient to @@ -34,8 +34,43 @@ def attach_votescount_to_queryset(queryset, as_field="votes_count"): """ model = queryset.model type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) - sql = ("SELECT coalesce(votes_votes.count, 0) FROM votes_votes " - "WHERE votes_votes.content_type_id = {type_id} AND votes_votes.object_id = {tbl}.id") + sql = """SELECT coalesce(SUM(total_voters), 0) FROM ( + SELECT coalesce(votes_votes.count, 0) total_voters + FROM votes_votes + WHERE votes_votes.content_type_id = {type_id} + AND votes_votes.object_id = {tbl}.id + ) as e""" + sql = sql.format(type_id=type.id, tbl=model._meta.db_table) qs = queryset.extra(select={as_field: sql}) return qs + + +def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"): + """Attach is_vote boolean to each object of the queryset. + + Because of laziness of vote objects creation, this makes much simpler and more efficient to + access to votes-object and check if the curren user vote it. + + (The other way was to do it in the serializer with some try/except blocks and additional + queries) + + :param user: A users.User object model + :param queryset: A Django queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM votes_vote + WHERE votes_vote.content_type_id = {type_id} + AND votes_vote.object_id = {tbl}.id + AND votes_vote.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + qs = queryset.extra(select={as_field: sql}) + return qs diff --git a/taiga/projects/wiki/admin.py b/taiga/projects/wiki/admin.py index cb846105..ed90f65c 100644 --- a/taiga/projects/wiki/admin.py +++ b/taiga/projects/wiki/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,6 +17,9 @@ from django.contrib import admin from taiga.projects.attachments.admin import AttachmentInline +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline + from taiga.projects.wiki.models import WikiPage from . import models @@ -24,7 +27,7 @@ from . import models class WikiPageAdmin(admin.ModelAdmin): list_display = ["project", "slug", "owner"] list_display_links = list_display - # inlines = [AttachmentInline] + inlines = [WatchedInline, VoteInline] admin.site.register(models.WikiPage, WikiPageAdmin) diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index a2e39eda..60cfe8c0 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -21,13 +21,13 @@ from taiga.base.api.permissions import IsAuthenticated from taiga.base import filters from taiga.base import exceptions as exc from taiga.base import response -from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route from taiga.projects.models import Project from taiga.mdrender.service import render as mdrender -from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.occ import OCCResourceMixin @@ -43,6 +43,12 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, permission_classes = (permissions.WikiPagePermission,) filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ("project", "slug") + queryset = models.WikiPage.objects.all() + + def get_queryset(self): + qs = super().get_queryset() + qs = self.attach_watchers_attrs_to_queryset(qs) + return qs @list_route(methods=["GET"]) def by_slug(self, request): @@ -77,6 +83,11 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, super().pre_save(obj) +class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.WikiPageWatchersPermission,) + resource_model = models.WikiPage + + class WikiLinkViewSet(ModelCrudViewSet): model = models.WikiLink serializer_class = serializers.WikiLinkSerializer diff --git a/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py b/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py new file mode 100644 index 00000000..44d6d5ac --- /dev/null +++ b/taiga/projects/wiki/migrations/0002_remove_wikipage_watchers.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import connection +from django.db import models, migrations +from django.contrib.contenttypes.models import ContentType +from taiga.base.utils.contenttypes import update_all_contenttypes + +def create_notifications(apps, schema_editor): + update_all_contenttypes(verbosity=0) + sql=""" +INSERT INTO notifications_watched (object_id, created_date, content_type_id, user_id, project_id) +SELECT wikipage_id AS object_id, now() AS created_date, {content_type_id} AS content_type_id, user_id, project_id +FROM wiki_wikipage_watchers INNER JOIN wiki_wikipage ON wiki_wikipage_watchers.wikipage_id = wiki_wikipage.id""".format(content_type_id=ContentType.objects.get(model='wikipage').id) + cursor = connection.cursor() + cursor.execute(sql) + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0004_watched'), + ('wiki', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_notifications), + migrations.RemoveField( + model_name='wikipage', + name='watchers', + ), + ] diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index b299c3ff..de3cb25c 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py index 684880a8..c1dd1e74 100644 --- a/taiga/projects/wiki/permissions.py +++ b/taiga/projects/wiki/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,7 +15,8 @@ # along with this program. If not, see . from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectOwner, AllowAny, IsSuperUser) + IsAuthenticated, IsProjectOwner, AllowAny, + IsSuperUser) class WikiPagePermission(TaigaResourcePermission): @@ -29,6 +30,16 @@ class WikiPagePermission(TaigaResourcePermission): destroy_perms = HasProjectPerm('delete_wiki_page') list_perms = AllowAny() render_perms = AllowAny() + watch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_wiki_pages') + + +class WikiPageWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectOwner() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_wiki_pages') + list_perms = HasProjectPerm('view_wiki_pages') + class WikiLinkPermission(TaigaResourcePermission): enought_perms = IsProjectOwner() | IsSuperUser() diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py index 71588c8d..d1dfc938 100644 --- a/taiga/projects/wiki/serializers.py +++ b/taiga/projects/wiki/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,15 +15,15 @@ # along with this program. If not, see . from taiga.base.api import serializers +from taiga.projects.history import services as history_service +from taiga.projects.notifications.mixins import WatchedResourceModelSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.mdrender.service import render as mdrender from . import models -from taiga.projects.history import services as history_service -from taiga.mdrender.service import render as mdrender - - -class WikiPageSerializer(serializers.ModelSerializer): +class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer): html = serializers.SerializerMethodField("get_html") editions = serializers.SerializerMethodField("get_editions") @@ -39,7 +39,6 @@ class WikiPageSerializer(serializers.ModelSerializer): class WikiLinkSerializer(serializers.ModelSerializer): - class Meta: model = models.WikiLink read_only_fields = ('href',) diff --git a/taiga/routers.py b/taiga/routers.py index 0f8bc675..5e972b83 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -1,7 +1,6 @@ - -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -49,6 +48,8 @@ router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notification # Projects & Selectors from taiga.projects.api import ProjectViewSet +from taiga.projects.api import ProjectFansViewSet +from taiga.projects.api import ProjectWatchersViewSet from taiga.projects.api import MembershipViewSet from taiga.projects.api import InvitationViewSet from taiga.projects.api import UserStoryStatusViewSet @@ -61,6 +62,8 @@ from taiga.projects.api import SeverityViewSet from taiga.projects.api import ProjectTemplateViewSet router.register(r"projects", ProjectViewSet, base_name="projects") +router.register(r"projects/(?P\d+)/fans", ProjectFansViewSet, base_name="project-fans") +router.register(r"projects/(?P\d+)/watchers", ProjectWatchersViewSet, base_name="project-watchers") router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates") router.register(r"memberships", MembershipViewSet, base_name="memberships") router.register(r"invitations", InvitationViewSet, base_name="invitations") @@ -123,21 +126,38 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-atta # Project components from taiga.projects.milestones.api import MilestoneViewSet +from taiga.projects.milestones.api import MilestoneWatchersViewSet from taiga.projects.userstories.api import UserStoryViewSet +from taiga.projects.userstories.api import UserStoryVotersViewSet +from taiga.projects.userstories.api import UserStoryWatchersViewSet from taiga.projects.tasks.api import TaskViewSet +from taiga.projects.tasks.api import TaskVotersViewSet +from taiga.projects.tasks.api import TaskWatchersViewSet from taiga.projects.issues.api import IssueViewSet -from taiga.projects.issues.api import VotersViewSet -from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet +from taiga.projects.issues.api import IssueVotersViewSet +from taiga.projects.issues.api import IssueWatchersViewSet +from taiga.projects.wiki.api import WikiViewSet +from taiga.projects.wiki.api import WikiLinkViewSet +from taiga.projects.wiki.api import WikiWatchersViewSet router.register(r"milestones", MilestoneViewSet, base_name="milestones") +router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers") router.register(r"userstories", UserStoryViewSet, base_name="userstories") +router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters") +router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers") router.register(r"tasks", TaskViewSet, base_name="tasks") +router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, base_name="task-voters") +router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers") router.register(r"issues", IssueViewSet, base_name="issues") -router.register(r"issues/(?P\d+)/voters", VotersViewSet, base_name="issue-voters") +router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet, base_name="issue-voters") +router.register(r"issues/(?P\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers") router.register(r"wiki", WikiViewSet, base_name="wiki") +router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers") router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") + + # History & Components from taiga.projects.history.api import UserStoryHistory from taiga.projects.history.api import TaskHistory @@ -193,6 +213,13 @@ router.register(r"importer", ProjectImporterViewSet, base_name="importer") router.register(r"exporter", ProjectExporterViewSet, base_name="exporter") +# External apps +from taiga.external_apps.api import Application, ApplicationToken +router.register(r"applications", Application, base_name="applications") +router.register(r"application-tokens", ApplicationToken, base_name="application-tokens") + + + # Stats # - see taiga.stats.routers and taiga.stats.apps diff --git a/taiga/searches/api.py b/taiga/searches/api.py index 6aa2060d..5a985664 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -20,15 +20,14 @@ from taiga.base.api import viewsets from taiga.base import response from taiga.base.api.utils import get_object_or_404 -from taiga.projects.userstories.serializers import UserStorySerializer -from taiga.projects.tasks.serializers import TaskSerializer -from taiga.projects.issues.serializers import IssueSerializer -from taiga.projects.wiki.serializers import WikiPageSerializer from taiga.permissions.service import user_has_perm from . import services +from . import serializers +from concurrent import futures + class SearchViewSet(viewsets.ViewSet): def list(self, request, **kwargs): text = request.QUERY_PARAMS.get('text', "") @@ -37,14 +36,33 @@ class SearchViewSet(viewsets.ViewSet): project = self._get_project(project_id) result = {} - if user_has_perm(request.user, "view_us", project): - result["userstories"] = self._search_user_stories(project, text) - if user_has_perm(request.user, "view_tasks", project): - result["tasks"] = self._search_tasks(project, text) - if user_has_perm(request.user, "view_issues", project): - result["issues"] = self._search_issues(project, text) - if user_has_perm(request.user, "view_wiki_pages", project): - result["wikipages"] = self._search_wiki_pages(project, text) + with futures.ThreadPoolExecutor(max_workers=4) as executor: + futures_list = [] + if user_has_perm(request.user, "view_us", project): + uss_future = executor.submit(self._search_user_stories, project, text) + uss_future.result_key = "userstories" + futures_list.append(uss_future) + if user_has_perm(request.user, "view_tasks", project): + tasks_future = executor.submit(self._search_tasks, project, text) + tasks_future.result_key = "tasks" + futures_list.append(tasks_future) + if user_has_perm(request.user, "view_issues", project): + issues_future = executor.submit(self._search_issues, project, text) + issues_future.result_key = "issues" + futures_list.append(issues_future) + if user_has_perm(request.user, "view_wiki_pages", project): + wiki_pages_future = executor.submit(self._search_wiki_pages, project, text) + wiki_pages_future.result_key = "wikipages" + futures_list.append(wiki_pages_future) + + for future in futures.as_completed(futures_list): + data = [] + try: + data = future.result() + except Exception as exc: + print('%s generated an exception: %s' % (future.result_key, exc)) + finally: + result[future.result_key] = data result["count"] = sum(map(lambda x: len(x), result.values())) return response.Ok(result) @@ -55,20 +73,20 @@ class SearchViewSet(viewsets.ViewSet): def _search_user_stories(self, project, text): queryset = services.search_user_stories(project, text) - serializer = UserStorySerializer(queryset, many=True) + serializer = serializers.UserStorySearchResultsSerializer(queryset, many=True) return serializer.data def _search_tasks(self, project, text): queryset = services.search_tasks(project, text) - serializer = TaskSerializer(queryset, many=True) + serializer = serializers.TaskSearchResultsSerializer(queryset, many=True) return serializer.data def _search_issues(self, project, text): queryset = services.search_issues(project, text) - serializer = IssueSerializer(queryset, many=True) + serializer = serializers.IssueSearchResultsSerializer(queryset, many=True) return serializer.data def _search_wiki_pages(self, project, text): queryset = services.search_wiki_pages(project, text) - serializer = WikiPageSerializer(queryset, many=True) + serializer = serializers.WikiPageSearchResultsSerializer(queryset, many=True) return serializer.data diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py new file mode 100644 index 00000000..3ec5f289 --- /dev/null +++ b/taiga/searches/serializers.py @@ -0,0 +1,49 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.projects.issues.serializers import IssueSerializer +from taiga.projects.userstories.serializers import UserStorySerializer +from taiga.projects.tasks.serializers import TaskSerializer +from taiga.projects.wiki.serializers import WikiPageSerializer + +from taiga.projects.issues.models import Issue +from taiga.projects.userstories.models import UserStory +from taiga.projects.tasks.models import Task +from taiga.projects.wiki.models import WikiPage + + +class IssueSearchResultsSerializer(IssueSerializer): + class Meta: + model = Issue + fields = ('id', 'ref', 'subject', 'status', 'assigned_to') + + +class TaskSearchResultsSerializer(TaskSerializer): + class Meta: + model = Task + fields = ('id', 'ref', 'subject', 'status', 'assigned_to') + + +class UserStorySearchResultsSerializer(UserStorySerializer): + class Meta: + model = UserStory + fields = ('id', 'ref', 'subject', 'status', 'total_points') + + +class WikiPageSearchResultsSerializer(WikiPageSerializer): + class Meta: + model = WikiPage + fields = ('id', 'slug') diff --git a/taiga/searches/services.py b/taiga/searches/services.py index 6786df83..d2f84798 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -16,19 +16,20 @@ from django.apps import apps from django.conf import settings - +from taiga.base.utils.db import to_tsquery MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) def search_user_stories(project, text): model_cls = apps.get_model("userstories", "UserStory") - where_clause = ("to_tsvector(coalesce(userstories_userstory.subject) || ' ' || " - "coalesce(userstories_userstory.ref) || ' ' || " - "coalesce(userstories_userstory.description)) @@ plainto_tsquery(%s)") + where_clause = ("to_tsvector('english_nostop', coalesce(userstories_userstory.subject) || ' ' || " + "coalesce(userstories_userstory.ref) || ' ' || " + "coalesce(userstories_userstory.description, '')) " + "@@ to_tsquery('english_nostop', %s)") if text: - return (model_cls.objects.extra(where=[where_clause], params=[text]) + return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) .filter(project_id=project.pk)[:MAX_RESULTS]) return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] @@ -36,12 +37,12 @@ def search_user_stories(project, text): def search_tasks(project, text): model_cls = apps.get_model("tasks", "Task") - where_clause = ("to_tsvector(coalesce(tasks_task.subject, '') || ' ' || " + where_clause = ("to_tsvector('english_nostop', coalesce(tasks_task.subject, '') || ' ' || " "coalesce(tasks_task.ref) || ' ' || " - "coalesce(tasks_task.description, '')) @@ plainto_tsquery(%s)") + "coalesce(tasks_task.description, '')) @@ to_tsquery('english_nostop', %s)") if text: - return (model_cls.objects.extra(where=[where_clause], params=[text]) + return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) .filter(project_id=project.pk)[:MAX_RESULTS]) return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] @@ -49,12 +50,12 @@ def search_tasks(project, text): def search_issues(project, text): model_cls = apps.get_model("issues", "Issue") - where_clause = ("to_tsvector(coalesce(issues_issue.subject) || ' ' || " + where_clause = ("to_tsvector('english_nostop', coalesce(issues_issue.subject) || ' ' || " "coalesce(issues_issue.ref) || ' ' || " - "coalesce(issues_issue.description)) @@ plainto_tsquery(%s)") + "coalesce(issues_issue.description, '')) @@ to_tsquery('english_nostop', %s)") if text: - return (model_cls.objects.extra(where=[where_clause], params=[text]) + return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) .filter(project_id=project.pk)[:MAX_RESULTS]) return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] @@ -62,11 +63,12 @@ def search_issues(project, text): def search_wiki_pages(project, text): model_cls = apps.get_model("wiki", "WikiPage") - where_clause = ("to_tsvector(coalesce(wiki_wikipage.slug) || ' ' || coalesce(wiki_wikipage.content)) " - "@@ plainto_tsquery(%s)") + where_clause = ("to_tsvector('english_nostop', coalesce(wiki_wikipage.slug) || ' ' || " + "coalesce(wiki_wikipage.content, '')) " + "@@ to_tsquery('english_nostop', %s)") if text: - return (model_cls.objects.extra(where=[where_clause], params=[text]) + return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) .filter(project_id=project.pk)[:MAX_RESULTS]) return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] diff --git a/taiga/stats/__init__.py b/taiga/stats/__init__.py index 92ed9d3d..28eceffa 100644 --- a/taiga/stats/__init__.py +++ b/taiga/stats/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 Taiga Agile LLC # 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 diff --git a/taiga/stats/api.py b/taiga/stats/api.py index bb2948e4..ab765ed9 100644 --- a/taiga/stats/api.py +++ b/taiga/stats/api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 Taiga Agile LLC # 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 diff --git a/taiga/stats/apps.py b/taiga/stats/apps.py index e8b8e63a..dc893e5c 100644 --- a/taiga/stats/apps.py +++ b/taiga/stats/apps.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 Taiga Agile LLC # 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 diff --git a/taiga/stats/permissions.py b/taiga/stats/permissions.py index 0eb1a362..16fb72a8 100644 --- a/taiga/stats/permissions.py +++ b/taiga/stats/permissions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 Taiga Agile LLC # 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 diff --git a/taiga/stats/routers.py b/taiga/stats/routers.py index 7257b473..dcab7819 100644 --- a/taiga/stats/routers.py +++ b/taiga/stats/routers.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 Taiga Agile LLC # 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 diff --git a/taiga/stats/services.py b/taiga/stats/services.py index 37980d03..61e30c51 100644 --- a/taiga/stats/services.py +++ b/taiga/stats/services.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015 Taiga Agile LLC +# Copyright (C) 2014-2015 Taiga Agile LLC # 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 @@ -12,10 +12,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from contextlib import closing from django.apps import apps -from django.db import connection +from django.db.models import Count from django.utils import timezone from datetime import timedelta from collections import OrderedDict @@ -37,26 +36,27 @@ def get_users_stats(): # Graph: users last year a_year_ago = timezone.now() - timedelta(days=365) - sql_query = """ - SELECT date_trunc('week', "filtered_users"."date_joined") AS "week", - count(*) - FROM (SELECT * - FROM "users_user" - WHERE "users_user"."is_active" = TRUE - AND "users_user"."is_system" = FALSE - AND "users_user"."date_joined" >= %s) AS "filtered_users" - GROUP BY "week" - ORDER BY "week"; - """ - with closing(connection.cursor()) as cursor: - cursor.execute(sql_query, [a_year_ago]) - rows = cursor.fetchall() + # increments -> + # SELECT date_trunc('week', "filtered_users"."date_joined") AS "week", + # count(*) + # FROM (SELECT * + # FROM "users_user" + # WHERE "users_user"."is_active" = TRUE + # AND "users_user"."is_system" = FALSE + # AND "users_user"."date_joined" >= %s) AS "filtered_users" + # GROUP BY "week" + # ORDER BY "week"; + increments = (queryset.filter(date_joined__gte=a_year_ago) + .extra({"week": "date_trunc('week', date_joined)"}) + .values("week") + .order_by("week") + .annotate(count=Count("id"))) counts_last_year_per_week = OrderedDict() - sumatory = queryset.filter(date_joined__lt=rows[0][0]).count() - for row in rows: - sumatory += row[1] - counts_last_year_per_week[str(row[0].date())] = sumatory + sumatory = queryset.filter(date_joined__lt=increments[0]["week"]).count() + for inc in increments: + sumatory += inc["count"] + counts_last_year_per_week[str(inc["week"].date())] = sumatory stats["counts_last_year_per_week"] = counts_last_year_per_week diff --git a/taiga/timeline/__init__.py b/taiga/timeline/__init__.py index 5e6960c6..21c6a821 100644 --- a/taiga/timeline/__init__.py +++ b/taiga/timeline/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index 851a693f..49504499 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index cbdea045..7b926f9e 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/management/commands/clear_unnecessary_new_membership_entries.py b/taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py similarity index 79% rename from taiga/timeline/management/commands/clear_unnecessary_new_membership_entries.py rename to taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py index f9b65dcf..b4233923 100644 --- a/taiga/timeline/management/commands/clear_unnecessary_new_membership_entries.py +++ b/taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,18 +15,16 @@ # along with this program. If not, see . from django.conf import settings from django.core.management.base import BaseCommand +from django.test.utils import override_settings from taiga.timeline.models import Timeline from taiga.projects.models import Project class Command(BaseCommand): help = 'Regenerate unnecessary new memberships entry lines' - def handle(self, *args, **options): - debug_enabled = settings.DEBUG - if debug_enabled: - print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)") - return + @override_settings(DEBUG=False) + def handle(self, *args, **options): removing_timeline_ids = [] for t in Timeline.objects.filter(event_type="projects.membership.create").order_by("created"): print(t.created) diff --git a/taiga/timeline/management/commands/rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py similarity index 90% rename from taiga/timeline/management/commands/rebuild_timeline_for_user_creation.py rename to taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py index 2982969a..bac46080 100644 --- a/taiga/timeline/management/commands/rebuild_timeline_for_user_creation.py +++ b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,6 +23,7 @@ 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) @@ -88,10 +89,6 @@ def generate_timeline(): class Command(BaseCommand): help = 'Regenerate project timeline' + @override_settings(DEBUG=False) def handle(self, *args, **options): - debug_enabled = settings.DEBUG - if debug_enabled: - print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)") - return - generate_timeline() diff --git a/taiga/timeline/management/commands/update_timeline_for_updated_tasks.py b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py similarity index 89% rename from taiga/timeline/management/commands/update_timeline_for_updated_tasks.py rename to taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py index 15b42172..ab1e4047 100644 --- a/taiga/timeline/management/commands/update_timeline_for_updated_tasks.py +++ b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -18,6 +18,7 @@ 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 Prefetch, F +from django.test.utils import override_settings from taiga.timeline.models import Timeline from taiga.timeline.timeline_implementations import userstory_timeline @@ -34,7 +35,7 @@ def update_timeline(initial_date, final_date): timelines = timelines.filter(created__lt=final_date) timelines = timelines.filter(event_type="tasks.task.change") - + print("Generating tasks indexed by id dict") task_ids = timelines.values_list("object_id", flat=True) tasks_per_id = {task.id: task for task in Task.objects.filter(id__in=task_ids).select_related("user_story").iterator()} @@ -77,10 +78,6 @@ class Command(BaseCommand): help='Final date for timeline update'), ) + @override_settings(DEBUG=False) def handle(self, *args, **options): - debug_enabled = settings.DEBUG - if debug_enabled: - print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)") - return - update_timeline(options["initial_date"], options["final_date"]) diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py index 2c462ce0..a7a72955 100644 --- a/taiga/timeline/management/commands/rebuild_timeline.py +++ b/taiga/timeline/management/commands/rebuild_timeline.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -25,6 +25,8 @@ 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 @@ -165,13 +167,8 @@ class Command(BaseCommand): help='Selected project id for timeline generation'), ) - + @override_settings(DEBUG=False) def handle(self, *args, **options): - debug_enabled = settings.DEBUG - if debug_enabled: - print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)") - return - if options["purge"] == True: Timeline.objects.all().delete() diff --git a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py index 527a85bf..008da7b8 100644 --- a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py +++ b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index ac181842..c92a87c8 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/permissions.py b/taiga/timeline/permissions.py index f07ee929..5aae5df4 100644 --- a/taiga/timeline/permissions.py +++ b/taiga/timeline/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index 85a8c77f..f5defc56 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 0fda608d..cc03d676 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -96,7 +96,7 @@ def get_timeline(obj, namespace=None): if namespace is not None: timeline = timeline.filter(namespace=namespace) - timeline = timeline.order_by("-created") + timeline = timeline.order_by("-created", "-id") return timeline diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 7769817d..0aafb831 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -22,14 +22,12 @@ 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, extract_user_info) -# TODO: Add events to followers timeline when followers are implemented. -# TODO: Add events to project watchers timeline when project watchers are implemented. - def _push_to_timeline(*args, **kwargs): if settings.CELERY_ENABLED: @@ -47,31 +45,12 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d namespace=build_project_namespace(project), extra_data=extra_data) - ## User profile timelines - ## - Me - related_people = User.objects.filter(id=user.id) + if hasattr(obj, "get_related_people"): + related_people = obj.get_related_people() - ## - Owner - if hasattr(obj, "owner_id") and obj.owner_id: - related_people |= User.objects.filter(id=obj.owner_id) - - ## - Assigned to - if hasattr(obj, "assigned_to_id") and obj.assigned_to_id: - related_people |= User.objects.filter(id=obj.assigned_to_id) - - ## - Watchers - watchers = getattr(obj, "watchers", None) - if watchers: - related_people |= obj.watchers.all() - - ## - Exclude inactive and system users and remove duplicate - related_people = related_people.exclude(is_active=False) - related_people = related_people.exclude(is_system=True) - related_people = related_people.distinct() - - _push_to_timeline(related_people, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + _push_to_timeline(related_people, obj, event_type, created_datetime, + namespace=build_user_namespace(user), + extra_data=extra_data) else: # Actions not related with a project ## - Me diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py index 2b911607..8d85652c 100644 --- a/taiga/timeline/timeline_implementations.py +++ b/taiga/timeline/timeline_implementations.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/urls.py b/taiga/urls.py index 416da685..33407f0f 100644 --- a/taiga/urls.py +++ b/taiga/urls.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/users/admin.py b/taiga/users/admin.py index b2cd50cf..57055b2c 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/users/api.py b/taiga/users/api.py index 33ec137c..d72d021d 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -33,14 +33,11 @@ from taiga.base.api import ModelCrudViewSet from taiga.base.filters import PermissionBasedFilterBackend from taiga.base.api.utils import get_object_or_404 from taiga.base.filters import MembersFilterBackend +from taiga.base.mails import mail_builder from taiga.projects.votes import services as votes_service -from taiga.projects.serializers import StarredSerializer from easy_thumbnails.source_generators import pil_image -from djmail.template_mail import MagicMailBuilder -from djmail.template_mail import InlineCSSTemplateMail - from . import models from . import serializers from . import permissions @@ -59,7 +56,7 @@ class UsersViewSet(ModelCrudViewSet): def get_serializer_class(self): if self.action in ["partial_update", "update", "retrieve", "by_username"]: user = self.object - if self.request.user == user: + if self.request.user == user or self.request.user.is_superuser: return self.admin_serializer_class return self.serializer_class @@ -80,39 +77,64 @@ class UsersViewSet(ModelCrudViewSet): return response.Ok(serializer.data) - @list_route(methods=["GET"]) - def by_username(self, request, *args, **kwargs): - username = request.QUERY_PARAMS.get("username", None) - return self.retrieve(request, username=username) - def retrieve(self, request, *args, **kwargs): self.object = get_object_or_404(models.User, **kwargs) self.check_permissions(request, 'retrieve', self.object) serializer = self.get_serializer(self.object) return response.Ok(serializer.data) - @detail_route(methods=["GET"]) - def contacts(self, request, *args, **kwargs): - user = get_object_or_404(models.User, **kwargs) - self.check_permissions(request, 'contacts', user) + #TODO: commit_on_success + def partial_update(self, request, *args, **kwargs): + """ + We must detect if the user is trying to change his email so we can + save that value and generate a token that allows him to validate it in + the new email account + """ + user = self.get_object() + self.check_permissions(request, "update", user) - self.object_list = user_filters.ContactsFilterBackend().filter_queryset( - user, request, self.get_queryset(), self).extra( - select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name") + ret = super().partial_update(request, *args, **kwargs) - page = self.paginate_queryset(self.object_list) - if page is not None: - serializer = self.serializer_class(page.object_list, many=True) - else: - serializer = self.serializer_class(self.object_list, many=True) + new_email = request.DATA.get('email', None) + if new_email is not None: + valid_new_email = True + duplicated_email = models.User.objects.filter(email = new_email).exists() - return response.Ok(serializer.data) + try: + validate_email(new_email) + except ValidationError: + valid_new_email = False - @detail_route(methods=["GET"]) - def stats(self, request, *args, **kwargs): - user = get_object_or_404(models.User, **kwargs) - self.check_permissions(request, "stats", user) - return response.Ok(services.get_stats_for_user(user, request.user)) + valid_new_email = valid_new_email and new_email != request.user.email + + if duplicated_email: + raise exc.WrongArguments(_("Duplicated email")) + elif not valid_new_email: + raise exc.WrongArguments(_("Not valid email")) + + #We need to generate a token for the email + request.user.email_token = str(uuid.uuid1()) + request.user.new_email = new_email + request.user.save(update_fields=["email_token", "new_email"]) + email = mail_builder.change_email(request.user.new_email, {"user": request.user, + "lang": request.user.lang}) + email.send() + + return ret + + def destroy(self, request, pk=None): + user = self.get_object() + self.check_permissions(request, "destroy", user) + stream = request.stream + request_data = stream is not None and stream.GET or None + user_cancel_account_signal.send(sender=user.__class__, user=user, request_data=request_data) + user.cancel() + return response.NoContent() + + @list_route(methods=["GET"]) + def by_username(self, request, *args, **kwargs): + username = request.QUERY_PARAMS.get("username", None) + return self.retrieve(request, username=username) @list_route(methods=["POST"]) def password_recovery(self, request, pk=None): @@ -133,8 +155,7 @@ class UsersViewSet(ModelCrudViewSet): user.token = str(uuid.uuid1()) user.save(update_fields=["token"]) - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - email = mbuilder.password_recovery(user, {"user": user}) + email = mail_builder.password_recovery(user, {"user": user}) email.send() return response.Ok({"detail": _("Mail sended successful!")}) @@ -207,6 +228,7 @@ class UsersViewSet(ModelCrudViewSet): except Exception: raise exc.WrongArguments(_("Invalid image format")) + request.user.delete_photo() request.user.photo = avatar request.user.save(update_fields=["photo"]) user_data = self.admin_serializer_class(request.user).data @@ -219,60 +241,11 @@ class UsersViewSet(ModelCrudViewSet): Remove the avatar of current logged user. """ self.check_permissions(request, "remove_avatar", None) - request.user.photo = None + request.user.delete_photo() request.user.save(update_fields=["photo"]) user_data = self.admin_serializer_class(request.user).data return response.Ok(user_data) - @detail_route(methods=["GET"]) - def starred(self, request, pk=None): - user = self.get_object() - self.check_permissions(request, 'starred', user) - - stars = votes_service.get_voted(user.pk, model=apps.get_model('projects', 'Project')) - stars_data = StarredSerializer(stars, many=True) - return response.Ok(stars_data.data) - - #TODO: commit_on_success - def partial_update(self, request, *args, **kwargs): - """ - We must detect if the user is trying to change his email so we can - save that value and generate a token that allows him to validate it in - the new email account - """ - user = self.get_object() - self.check_permissions(request, "update", user) - - ret = super(UsersViewSet, self).partial_update(request, *args, **kwargs) - - new_email = request.DATA.get('email', None) - if new_email is not None: - valid_new_email = True - duplicated_email = models.User.objects.filter(email = new_email).exists() - - try: - validate_email(new_email) - except ValidationError: - valid_new_email = False - - valid_new_email = valid_new_email and new_email != request.user.email - - if duplicated_email: - raise exc.WrongArguments(_("Duplicated email")) - elif not valid_new_email: - raise exc.WrongArguments(_("Not valid email")) - - #We need to generate a token for the email - request.user.email_token = str(uuid.uuid1()) - request.user.new_email = new_email - request.user.save(update_fields=["email_token", "new_email"]) - mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - email = mbuilder.change_email(request.user.new_email, {"user": request.user, - "lang": request.user.lang}) - email.send() - - return ret - @list_route(methods=["POST"]) def change_email(self, request, pk=None): """ @@ -329,15 +302,108 @@ class UsersViewSet(ModelCrudViewSet): user.cancel() return response.NoContent() - def destroy(self, request, pk=None): - user = self.get_object() - self.check_permissions(request, "destroy", user) - stream = request.stream - request_data = stream is not None and stream.GET or None - user_cancel_account_signal.send(sender=user.__class__, user=user, request_data=request_data) - user.cancel() - return response.NoContent() + @detail_route(methods=["GET"]) + def contacts(self, request, *args, **kwargs): + user = get_object_or_404(models.User, **kwargs) + self.check_permissions(request, 'contacts', user) + self.object_list = user_filters.ContactsFilterBackend().filter_queryset( + user, request, self.get_queryset(), self).extra( + select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name") + + page = self.paginate_queryset(self.object_list) + if page is not None: + serializer = self.serializer_class(page.object_list, many=True) + else: + serializer = self.serializer_class(self.object_list, many=True) + + return response.Ok(serializer.data) + + @detail_route(methods=["GET"]) + def stats(self, request, *args, **kwargs): + user = get_object_or_404(models.User, **kwargs) + self.check_permissions(request, "stats", user) + return response.Ok(services.get_stats_for_user(user, request.user)) + + @detail_route(methods=["GET"]) + def watched(self, request, *args, **kwargs): + for_user = get_object_or_404(models.User, **kwargs) + from_user = request.user + self.check_permissions(request, 'watched', for_user) + filters = { + "type": request.GET.get("type", None), + "q": request.GET.get("q", None), + } + + self.object_list = services.get_watched_list(for_user, from_user, **filters) + page = self.paginate_queryset(self.object_list) + elements = page.object_list if page is not None else self.object_list + + extra_args_liked = { + "user_watching": services.get_watched_content_for_user(request.user), + "user_likes": services.get_liked_content_for_user(request.user), + } + + extra_args_voted = { + "user_watching": services.get_watched_content_for_user(request.user), + "user_votes": services.get_voted_content_for_user(request.user), + } + + response_data = [] + for elem in elements: + if elem["type"] == "project": + # projects are liked objects + response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data ) + else: + # stories, tasks and issues are voted objects + response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data ) + + return response.Ok(response_data) + + @detail_route(methods=["GET"]) + def liked(self, request, *args, **kwargs): + for_user = get_object_or_404(models.User, **kwargs) + from_user = request.user + self.check_permissions(request, 'liked', for_user) + filters = { + "q": request.GET.get("q", None), + } + + self.object_list = services.get_liked_list(for_user, from_user, **filters) + page = self.paginate_queryset(self.object_list) + elements = page.object_list if page is not None else self.object_list + + extra_args = { + "user_watching": services.get_watched_content_for_user(request.user), + "user_likes": services.get_liked_content_for_user(request.user), + } + + response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements] + + return response.Ok(response_data) + + @detail_route(methods=["GET"]) + def voted(self, request, *args, **kwargs): + for_user = get_object_or_404(models.User, **kwargs) + from_user = request.user + self.check_permissions(request, 'liked', for_user) + filters = { + "type": request.GET.get("type", None), + "q": request.GET.get("q", None), + } + + self.object_list = services.get_voted_list(for_user, from_user, **filters) + page = self.paginate_queryset(self.object_list) + elements = page.object_list if page is not None else self.object_list + + extra_args = { + "user_watching": services.get_watched_content_for_user(request.user), + "user_votes": services.get_voted_content_for_user(request.user), + } + + response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements] + + return response.Ok(response_data) ###################################################### ## Role diff --git a/taiga/users/filters.py b/taiga/users/filters.py index df7f04ae..8d2704b3 100644 --- a/taiga/users/filters.py +++ b/taiga/users/filters.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/users/fixtures/initial_user.json b/taiga/users/fixtures/initial_user.json index ed7833c1..1b1e7dd8 100644 --- a/taiga/users/fixtures/initial_user.json +++ b/taiga/users/fixtures/initial_user.json @@ -3,7 +3,7 @@ "model": "users.user", "fields": { "username": "admin", - "full_name": "", + "full_name": "Administrator", "bio": "", "lang": "", "color": "", diff --git a/taiga/users/forms.py b/taiga/users/forms.py index 1e7bb7ab..7d9521a6 100644 --- a/taiga/users/forms.py +++ b/taiga/users/forms.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -41,4 +41,4 @@ class UserCreationForm(DjangoUserCreationForm): class UserChangeForm(DjangoUserChangeForm): class Meta: model = User - + fields = '__all__' diff --git a/taiga/users/gravatar.py b/taiga/users/gravatar.py index aaae895f..f26fde9d 100644 --- a/taiga/users/gravatar.py +++ b/taiga/users/gravatar.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/users/migrations/0012_auto_20150812_1142.py b/taiga/users/migrations/0012_auto_20150812_1142.py new file mode 100644 index 00000000..fff8c17b --- /dev/null +++ b/taiga/users/migrations/0012_auto_20150812_1142.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import djorm_pgarray.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0011_user_theme'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='permissions', + field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('star_project', 'Star project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('vote_us', 'Vote user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('vote_task', 'Vote task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('vote_issue', 'Vote issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], verbose_name='permissions', default=[], dbtype='text'), + preserve_default=True, + ), + ] diff --git a/taiga/users/migrations/0013_auto_20150901_1600.py b/taiga/users/migrations/0013_auto_20150901_1600.py new file mode 100644 index 00000000..8d2e4143 --- /dev/null +++ b/taiga/users/migrations/0013_auto_20150901_1600.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import djorm_pgarray.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0012_auto_20150812_1142'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='permissions', + field=djorm_pgarray.fields.TextArrayField(default=[], choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', verbose_name='permissions'), + preserve_default=True, + ), + ] diff --git a/taiga/users/migrations/0014_auto_20151005_1357.py b/taiga/users/migrations/0014_auto_20151005_1357.py new file mode 100644 index 00000000..ee24e169 --- /dev/null +++ b/taiga/users/migrations/0014_auto_20151005_1357.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.contrib.auth.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0013_auto_20150901_1600'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.AlterField( + model_name='user', + name='last_login', + field=models.DateTimeField(verbose_name='last login', blank=True, null=True), + ), + migrations.AlterField( + model_name='user', + name='new_email', + field=models.EmailField(verbose_name='new email address', blank=True, null=True, max_length=254), + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index 345115be..b2828912 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -40,6 +40,8 @@ from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.iterators import split_by_n from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from easy_thumbnails.files import get_thumbnailer + def generate_random_hex_color(): return "#{:06x}".format(random.randint(0,0xFFFFFF)) @@ -174,9 +176,22 @@ class User(AbstractBaseUser, PermissionsMixin): self.colorize_tags = True self.token = None self.set_unusable_password() + self.delete_photo() self.save() self.auth_data.all().delete() + def delete_photo(self): + # Removing thumbnails + thumbnailer = get_thumbnailer(self.photo) + thumbnailer.delete_thumbnails() + + # Removing original photo + if self.photo: + storage, path = self.photo.storage, self.photo.path + storage.delete(path) + + self.photo = None + class Role(models.Model): name = models.CharField(max_length=200, null=False, blank=False, diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py index bad16f1a..de72dd85 100644 --- a/taiga/users/permissions.py +++ b/taiga/users/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -44,9 +44,11 @@ class UserPermission(TaigaResourcePermission): change_avatar_perms = IsAuthenticated() me_perms = IsAuthenticated() remove_avatar_perms = IsAuthenticated() - starred_perms = AllowAny() change_email_perms = AllowAny() contacts_perms = AllowAny() + liked_perms = AllowAny() + voted_perms = AllowAny() + watched_perms = AllowAny() class RolesPermission(TaigaResourcePermission): diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 934c0677..759e5a0f 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -19,11 +19,14 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers -from taiga.base.fields import PgArrayField +from taiga.base.fields import PgArrayField, TagsField + from taiga.projects.models import Project from .models import User, Role from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url +from collections import namedtuple + import re @@ -108,10 +111,11 @@ class UserAdminSerializer(UserSerializer): read_only_fields = ("id", "email") -class BasicInfoSerializer(UserSerializer): +class UserBasicInfoSerializer(UserSerializer): class Meta: model = User - fields = ("username", "full_name_display","photo", "big_photo") + fields = ("username", "full_name_display","photo", "big_photo", "is_active") + class RecoverySerializer(serializers.Serializer): token = serializers.CharField(max_length=200) @@ -148,3 +152,124 @@ class ProjectRoleSerializer(serializers.ModelSerializer): model = Role fields = ('id', 'name', 'slug', 'order', 'computable') i18n_fields = ("name",) + + +###################################################### +## Like +###################################################### + + +class HighLightedContentSerializer(serializers.Serializer): + type = serializers.CharField() + id = serializers.IntegerField() + ref = serializers.IntegerField() + slug = serializers.CharField() + name = serializers.CharField() + subject = serializers.CharField() + description = serializers.SerializerMethodField("get_description") + assigned_to = serializers.IntegerField() + status = serializers.CharField() + status_color = serializers.CharField() + tags_colors = serializers.SerializerMethodField("get_tags_color") + created_date = serializers.DateTimeField() + is_private = serializers.SerializerMethodField("get_is_private") + + project = serializers.SerializerMethodField("get_project") + project_name = serializers.SerializerMethodField("get_project_name") + project_slug = serializers.SerializerMethodField("get_project_slug") + project_is_private = serializers.SerializerMethodField("get_project_is_private") + + assigned_to_username = serializers.CharField() + assigned_to_full_name = serializers.CharField() + assigned_to_photo = serializers.SerializerMethodField("get_photo") + + is_watcher = serializers.SerializerMethodField("get_is_watcher") + total_watchers = serializers.IntegerField() + + def __init__(self, *args, **kwargs): + # Don't pass the extra ids args up to the superclass + self.user_watching = kwargs.pop("user_watching", {}) + + # Instantiate the superclass normally + super().__init__(*args, **kwargs) + + def _none_if_project(self, obj, property): + type = obj.get("type", "") + if type == "project": + return None + + return obj.get(property) + + def _none_if_not_project(self, obj, property): + type = obj.get("type", "") + if type != "project": + return None + + return obj.get(property) + + def get_project(self, obj): + return self._none_if_project(obj, "project") + + def get_is_private(self, obj): + return self._none_if_not_project(obj, "project_is_private") + + def get_project_name(self, obj): + return self._none_if_project(obj, "project_name") + + def get_description(self, obj): + return self._none_if_not_project(obj, "description") + + def get_project_slug(self, obj): + return self._none_if_project(obj, "project_slug") + + def get_project_is_private(self, obj): + return self._none_if_project(obj, "project_is_private") + + def get_photo(self, obj): + type = obj.get("type", "") + if type == "project": + return None + + UserData = namedtuple("UserData", ["photo", "email"]) + user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "") + return get_photo_or_gravatar_url(user_data) + + def get_tags_color(self, obj): + tags = obj.get("tags", []) + tags = tags if tags is not None else [] + tags_colors = obj.get("tags_colors", []) + tags_colors = tags_colors if tags_colors is not None else [] + return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags] + + def get_is_watcher(self, obj): + return obj["id"] in self.user_watching.get(obj["type"], []) + + +class LikedObjectSerializer(HighLightedContentSerializer): + is_fan = serializers.SerializerMethodField("get_is_fan") + total_fans = serializers.IntegerField() + + def __init__(self, *args, **kwargs): + # Don't pass the extra ids args up to the superclass + self.user_likes = kwargs.pop("user_likes", {}) + + # Instantiate the superclass normally + super().__init__(*args, **kwargs) + + def get_is_fan(self, obj): + return obj["id"] in self.user_likes.get(obj["type"], []) + + +class VotedObjectSerializer(HighLightedContentSerializer): + is_voter = serializers.SerializerMethodField("get_is_voter") + total_voters = serializers.IntegerField() + + def __init__(self, *args, **kwargs): + # Don't pass the extra ids args up to the superclass + self.user_votes = kwargs.pop("user_votes", {}) + + # Instantiate the superclass normally + super().__init__(*args, **kwargs) + + def get_is_voter(self, obj): + return obj["id"] in self.user_votes.get(obj["type"], []) diff --git a/taiga/users/services.py b/taiga/users/services.py index caf27226..93707737 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -20,17 +20,22 @@ This model contains a domain logic for users application. from django.apps import apps from django.db.models import Q +from django.db import connection from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.exceptions import InvalidImageFormatError from taiga.base import exceptions as exc +from taiga.base.utils.db import to_tsquery from taiga.base.utils.urls import get_absolute_url - +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.services import get_projects_watched from .gravatar import get_gravatar_url +from django.conf import settings def get_and_validate_user(*, username:str, password:str) -> bool: """ @@ -67,7 +72,7 @@ def get_photo_or_gravatar_url(user): """Get the user's photo/gravatar url.""" if user: return get_photo_url(user.photo) if user.photo else get_gravatar_url(user.email) - return "" + return settings.GRAVATAR_DEFAULT_AVATAR def get_big_photo_url(photo): @@ -142,3 +147,406 @@ def get_stats_for_user(from_user, by_user): 'total_num_closed_userstories': total_num_closed_userstories, } return project_stats + + +def get_liked_content_for_user(user): + """Returns a dict where: + - The key is the content_type model + - The values are list of id's of the different objects liked by the user + """ + if user.is_anonymous(): + return {} + + user_likes = {} + for (ct_model, object_id) in user.likes.values_list("content_type__model", "object_id"): + list = user_likes.get(ct_model, []) + list.append(object_id) + user_likes[ct_model] = list + + return user_likes + + +def get_voted_content_for_user(user): + """Returns a dict where: + - The key is the content_type model + - The values are list of id's of the different objects voted by the user + """ + if user.is_anonymous(): + return {} + + user_votes = {} + for (ct_model, object_id) in user.votes.values_list("content_type__model", "object_id"): + list = user_votes.get(ct_model, []) + list.append(object_id) + user_votes[ct_model] = list + + return user_votes + + +def get_watched_content_for_user(user): + """Returns a dict where: + - The key is the content_type model + - The values are list of id's of the different objects watched by the user + """ + if user.is_anonymous(): + return {} + + user_watches = {} + for (ct_model, object_id) in user.watched.values_list("content_type__model", "object_id"): + list = user_watches.get(ct_model, []) + list.append(object_id) + user_watches[ct_model] = list + + #Now for projects, + projects_watched = get_projects_watched(user) + project_content_type_model=ContentType.objects.get(app_label="projects", model="project").model + user_watches[project_content_type_model] = projects_watched.values_list("id", flat=True) + + return user_watches + + +def _build_watched_sql_for_projects(for_user): + sql = """ + SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type, + tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project, + slug, projects_project.name, null::text AS subject, + notifications_notifypolicy.created_at as created_date, + coalesce(watchers, 0) AS total_watchers, coalesce(likes_likes.count, 0) AS total_fans, null::integer AS total_voters, + null::integer AS assigned_to, null::text as status, null::text as status_color + FROM notifications_notifypolicy + INNER JOIN projects_project + ON (projects_project.id = notifications_notifypolicy.project_id) + LEFT JOIN (SELECT project_id, count(*) watchers + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.notify_level != {none_notify_level} + GROUP BY project_id + ) type_watchers + ON projects_project.id = type_watchers.project_id + LEFT JOIN likes_likes + ON (projects_project.id = likes_likes.object_id AND {project_content_type_id} = likes_likes.content_type_id) + WHERE + notifications_notifypolicy.user_id = {for_user_id} + AND notifications_notifypolicy.notify_level != {none_notify_level} + """ + sql = sql.format( + for_user_id=for_user.id, + none_notify_level=NotifyLevel.none, + project_content_type_id=ContentType.objects.get(app_label="projects", model="project").id) + return sql + + +def _build_liked_sql_for_projects(for_user): + sql = """ + SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type, + tags, likes_like.object_id AS object_id, projects_project.id AS project, + slug, projects_project.name, null::text AS subject, + likes_like.created_date, + coalesce(watchers, 0) AS total_watchers, coalesce(likes_likes.count, 0) AS total_fans, + null::integer AS assigned_to, null::text as status, null::text as status_color + FROM likes_like + INNER JOIN projects_project + ON (projects_project.id = likes_like.object_id) + LEFT JOIN (SELECT project_id, count(*) watchers + FROM notifications_notifypolicy + WHERE notifications_notifypolicy.notify_level != {none_notify_level} + GROUP BY project_id + ) type_watchers + ON projects_project.id = type_watchers.project_id + LEFT JOIN likes_likes + ON (projects_project.id = likes_likes.object_id AND {project_content_type_id} = likes_likes.content_type_id) + WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id + """ + sql = sql.format( + for_user_id=for_user.id, + none_notify_level=NotifyLevel.none, + project_content_type_id=ContentType.objects.get(app_label="projects", model="project").id) + + return sql + + +def _build_sql_for_type(for_user, type, table_name, action_table, ref_column="ref", + project_column="project_id", assigned_to_column="assigned_to_id", + slug_column="slug", subject_column="subject"): + sql = """ + SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, + tags, {action_table}.object_id AS object_id, {table_name}.{project_column} AS project, + {slug_column} AS slug, null AS name, {subject_column} AS subject, + {action_table}.created_date, + coalesce(watchers, 0) AS total_watchers, null::integer AS total_fans, coalesce(votes_votes.count, 0) AS total_voters, + {assigned_to_column} AS assigned_to, projects_{type}status.name as status, projects_{type}status.color as status_color + FROM {action_table} + INNER JOIN django_content_type + ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}') + INNER JOIN {table_name} + ON ({table_name}.id = {action_table}.object_id) + INNER JOIN projects_{type}status + ON (projects_{type}status.id = {table_name}.status_id) + LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers + ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id + LEFT JOIN votes_votes + ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id) + WHERE {action_table}.user_id = {for_user_id} + """ + sql = sql.format(for_user_id=for_user.id, type=type, table_name=table_name, + action_table=action_table, ref_column = ref_column, + project_column=project_column, assigned_to_column=assigned_to_column, + slug_column=slug_column, subject_column=subject_column) + + return sql + + +def get_watched_list(for_user, from_user, type=None, q=None): + filters_sql = "" + and_needed = False + + if type: + filters_sql += " AND type = '{type}' ".format(type=type) + + if q: + filters_sql += """ AND ( + to_tsvector('english_nostop', coalesce(subject,'') || ' ' ||coalesce(entities.name,'') || ' ' ||coalesce(to_char(ref, '999'),'')) @@ to_tsquery('english_nostop', '{q}') + ) + """.format(q=to_tsquery(q)) + + sql = """ + -- 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, + 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} + UNION + {tasks_sql} + UNION + {issues_sql} + UNION + {projects_sql} + ) as entities + -- END Basic info + + -- BEGIN Project info + LEFT JOIN projects_project + ON (entities.project = projects_project.id) + -- END Project info + + -- BEGIN Assigned to user info + LEFT JOIN users_user + ON (assigned_to = users_user.id) + -- END Assigned to user info + + -- BEGIN Permissions checking + LEFT JOIN projects_membership + -- Here we check the memberbships from the user requesting the info + ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project) + + LEFT JOIN users_role + ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id) + + WHERE + -- public project + ( + projects_project.is_private = false + OR( + -- private project where the view_ permission is included in the user role for that project or in the anon permissions + projects_project.is_private = true + AND( + (entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'project' AND 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + ) + )) + -- END Permissions checking + {filters_sql} + + ORDER BY entities.created_date DESC; + """ + + from_user_id = -1 + if not from_user.is_anonymous(): + from_user_id = from_user.id + + sql = sql.format( + for_user_id=for_user.id, + from_user_id=from_user_id, + filters_sql=filters_sql, + userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "notifications_watched", slug_column="null"), + tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "notifications_watched", slug_column="null"), + issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "notifications_watched", slug_column="null"), + projects_sql=_build_watched_sql_for_projects(for_user)) + + cursor = connection.cursor() + cursor.execute(sql) + + desc = cursor.description + return [ + dict(zip([col[0] for col in desc], row)) + for row in cursor.fetchall() + ] + + +def get_liked_list(for_user, from_user, type=None, q=None): + filters_sql = "" + and_needed = False + + if type: + filters_sql += " AND type = '{type}' ".format(type=type) + + if q: + filters_sql += """ AND ( + to_tsvector('english_nostop', coalesce(subject,'') || ' ' ||coalesce(entities.name,'') || ' ' ||coalesce(to_char(ref, '999'),'')) @@ to_tsquery('english_nostop', '{q}') + ) + """.format(q=to_tsquery(q)) + + sql = """ + -- 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, + 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} + ) as entities + -- END Basic info + + -- BEGIN Project info + LEFT JOIN projects_project + ON (entities.project = projects_project.id) + -- END Project info + + -- BEGIN Assigned to user info + LEFT JOIN users_user + ON (assigned_to = users_user.id) + -- END Assigned to user info + + -- BEGIN Permissions checking + LEFT JOIN projects_membership + -- Here we check the memberbships from the user requesting the info + ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project) + + LEFT JOIN users_role + ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id) + + WHERE + -- public project + ( + projects_project.is_private = false + OR( + -- private project where the view_ permission is included in the user role for that project or in the anon permissions + projects_project.is_private = true + AND( + 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)) + ) + )) + -- END Permissions checking + {filters_sql} + + ORDER BY entities.created_date DESC; + """ + + from_user_id = -1 + if not from_user.is_anonymous(): + from_user_id = from_user.id + + sql = sql.format( + for_user_id=for_user.id, + from_user_id=from_user_id, + filters_sql=filters_sql, + projects_sql=_build_liked_sql_for_projects(for_user)) + + cursor = connection.cursor() + cursor.execute(sql) + + desc = cursor.description + return [ + dict(zip([col[0] for col in desc], row)) + for row in cursor.fetchall() + ] + + +def get_voted_list(for_user, from_user, type=None, q=None): + filters_sql = "" + and_needed = False + + if type: + filters_sql += " AND type = '{type}' ".format(type=type) + + if q: + filters_sql += """ AND ( + to_tsvector('english_nostop', coalesce(subject,'') || ' ' ||coalesce(entities.name,'') || ' ' ||coalesce(to_char(ref, '999'),'')) @@ to_tsquery('english_nostop', '{q}') + ) + """.format(q=to_tsquery(q)) + + sql = """ + -- 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, + 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} + UNION + {tasks_sql} + UNION + {issues_sql} + ) as entities + -- END Basic info + + -- BEGIN Project info + LEFT JOIN projects_project + ON (entities.project = projects_project.id) + -- END Project info + + -- BEGIN Assigned to user info + LEFT JOIN users_user + ON (assigned_to = users_user.id) + -- END Assigned to user info + + -- BEGIN Permissions checking + LEFT JOIN projects_membership + -- Here we check the memberbships from the user requesting the info + ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project) + + LEFT JOIN users_role + ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id) + + WHERE + -- public project + ( + projects_project.is_private = false + OR( + -- private project where the view_ permission is included in the user role for that project or in the anon permissions + projects_project.is_private = true + AND( + (entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + ) + )) + -- END Permissions checking + {filters_sql} + + ORDER BY entities.created_date DESC; + """ + + from_user_id = -1 + if not from_user.is_anonymous(): + from_user_id = from_user.id + + sql = sql.format( + for_user_id=for_user.id, + from_user_id=from_user_id, + filters_sql=filters_sql, + userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "votes_vote", slug_column="null"), + tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "votes_vote", slug_column="null"), + issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "votes_vote", slug_column="null")) + + cursor = connection.cursor() + cursor.execute(sql) + + desc = cursor.description + return [ + dict(zip([col[0] for col in desc], row)) + for row in cursor.fetchall() + ] diff --git a/taiga/users/signals.py b/taiga/users/signals.py index e61cec01..f1bac392 100644 --- a/taiga/users/signals.py +++ b/taiga/users/signals.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/users/validators.py b/taiga/users/validators.py index 6989e739..bc3a5a0c 100644 --- a/taiga/users/validators.py +++ b/taiga/users/validators.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py index 6755f3ac..b767e5d9 100644 --- a/taiga/userstorage/api.py +++ b/taiga/userstorage/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/userstorage/filters.py b/taiga/userstorage/filters.py index a46b1e61..31e1062c 100644 --- a/taiga/userstorage/filters.py +++ b/taiga/userstorage/filters.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/userstorage/models.py b/taiga/userstorage/models.py index 70dfb9ab..31be0aae 100644 --- a/taiga/userstorage/models.py +++ b/taiga/userstorage/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/userstorage/permissions.py b/taiga/userstorage/permissions.py index 24c1693d..e3a049cb 100644 --- a/taiga/userstorage/permissions.py +++ b/taiga/userstorage/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/userstorage/serializers.py b/taiga/userstorage/serializers.py index 92e01c3f..ecbac6a2 100644 --- a/taiga/userstorage/serializers.py +++ b/taiga/userstorage/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/webhooks/__init__.py b/taiga/webhooks/__init__.py index 4f9173d3..57095ae0 100644 --- a/taiga/webhooks/__init__.py +++ b/taiga/webhooks/__init__.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index cda88dd7..8b254d10 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py index f0444d68..589afa48 100644 --- a/taiga/webhooks/apps.py +++ b/taiga/webhooks/apps.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/webhooks/models.py b/taiga/webhooks/models.py index 0704b77b..94e253aa 100644 --- a/taiga/webhooks/models.py +++ b/taiga/webhooks/models.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py index 40463642..a2ef0207 100644 --- a/taiga/webhooks/permissions.py +++ b/taiga/webhooks/permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index e802c1b3..9ea5a120 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -23,8 +23,9 @@ from taiga.projects.userstories import models as us_models from taiga.projects.tasks import models as task_models from taiga.projects.issues import models as issue_models from taiga.projects.milestones import models as milestone_models -from taiga.projects.history import models as history_models from taiga.projects.wiki import models as wiki_models +from taiga.projects.history import models as history_models +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from .models import Webhook, WebhookLog @@ -103,7 +104,8 @@ class PointSerializer(serializers.Serializer): return obj.value -class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): +class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): tags = TagsField(default=[], required=False) external_reference = PgArrayField(required=False) owner = UserSerializer() @@ -119,7 +121,8 @@ class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializ return project.userstorycustomattributes.all() -class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): +class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): tags = TagsField(default=[], required=False) owner = UserSerializer() assigned_to = UserSerializer() @@ -132,7 +135,8 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.M return project.taskcustomattributes.all() -class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer): +class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): tags = TagsField(default=[], required=False) owner = UserSerializer() assigned_to = UserSerializer() diff --git a/taiga/webhooks/signal_handlers.py b/taiga/webhooks/signal_handlers.py index 0483b145..193b100b 100644 --- a/taiga/webhooks/signal_handlers.py +++ b/taiga/webhooks/signal_handlers.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -15,6 +15,7 @@ # along with this program. If not, see . from django.conf import settings +from django.utils import timezone from taiga.projects.history import services as history_service from taiga.projects.history.choices import HistoryType @@ -54,7 +55,7 @@ def on_new_history_entry(sender, instance, created, **kwargs): extra_args = [instance] elif instance.type == HistoryType.delete: task = tasks.delete_webhook - extra_args = [] + extra_args = [timezone.now()] for webhook in webhooks: args = [webhook["id"], webhook["url"], webhook["key"], obj] + extra_args diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index 679da3b8..1933ed63 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -1,6 +1,6 @@ # Copyright (C) 2013 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -112,11 +112,12 @@ def create_webhook(webhook_id, url, key, obj): @app.task -def delete_webhook(webhook_id, url, key, obj): +def delete_webhook(webhook_id, url, key, obj, deleted_date): data = {} data['data'] = _serialize(obj) data['action'] = "delete" data['type'] = _get_type(obj) + data['deleted_date'] = deleted_date return _send_request(webhook_id, url, key, data) diff --git a/tests/conftest.py b/tests/conftest.py index ed81ee80..fa3ae01f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/factories.py b/tests/factories.py index 4e9b9d0c..c44167c5 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -231,6 +231,7 @@ class UserStoryFactory(Factory): subject = factory.Sequence(lambda n: "User Story {}".format(n)) description = factory.Sequence(lambda n: "User Story {} description".format(n)) status = factory.SubFactory("tests.factories.UserStoryStatusFactory") + milestone = factory.SubFactory("tests.factories.MilestoneFactory") class UserStoryStatusFactory(Factory): @@ -411,14 +412,23 @@ class IssueCustomAttributesValuesFactory(Factory): issue = factory.SubFactory("tests.factories.IssueFactory") -# class FanFactory(Factory): -# project = factory.SubFactory("tests.factories.ProjectFactory") -# user = factory.SubFactory("tests.factories.UserFactory") +class LikeFactory(Factory): + class Meta: + model = "likes.Like" + strategy = factory.CREATE_STRATEGY + + content_type = factory.SubFactory("tests.factories.ContentTypeFactory") + object_id = factory.Sequence(lambda n: n) + user = factory.SubFactory("tests.factories.UserFactory") -# class StarsFactory(Factory): -# project = factory.SubFactory("tests.factories.ProjectFactory") -# count = 0 +class LikesFactory(Factory): + class Meta: + model = "likes.Likes" + strategy = factory.CREATE_STRATEGY + + content_type = factory.SubFactory("tests.factories.ContentTypeFactory") + object_id = factory.Sequence(lambda n: n) class VoteFactory(Factory): @@ -440,6 +450,17 @@ class VotesFactory(Factory): object_id = factory.Sequence(lambda n: n) +class WatchedFactory(Factory): + class Meta: + model = "notifications.Watched" + strategy = factory.CREATE_STRATEGY + + content_type = factory.SubFactory("tests.factories.ContentTypeFactory") + object_id = factory.Sequence(lambda n: n) + user = factory.SubFactory("tests.factories.UserFactory") + project = factory.SubFactory("tests.factories.ProjectFactory") + + class ContentTypeFactory(Factory): class Meta: model = "contenttypes.ContentType" @@ -470,6 +491,21 @@ class HistoryEntryFactory(Factory): type = 1 +class ApplicationFactory(Factory): + class Meta: + model = "external_apps.Application" + strategy = factory.CREATE_STRATEGY + + key = "testingkey" + +class ApplicationTokenFactory(Factory): + class Meta: + model = "external_apps.ApplicationToken" + strategy = factory.CREATE_STRATEGY + + application = factory.SubFactory("tests.factories.ApplicationFactory") + user = factory.SubFactory("tests.factories.UserFactory") + def create_issue(**kwargs): "Create an issue and along with its dependencies." owner = kwargs.pop("owner", None) diff --git a/tests/fixtures.py b/tests/fixtures.py index 97cbea87..96d6a8fc 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/resources_permissions/test_application_tokens_resources.py b/tests/integration/resources_permissions/test_application_tokens_resources.py new file mode 100644 index 00000000..cc4e3a5d --- /dev/null +++ b/tests/integration/resources_permissions/test_application_tokens_resources.py @@ -0,0 +1,145 @@ +from django.core.urlresolvers import reverse + +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 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.token = f.ApplicationTokenFactory(state="random-state") + m.registered_user_with_token = m.token.user + return m + + +def test_application_tokens_create(client, data): + url = reverse('application-tokens-list') + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + data = json.dumps({"application": data.token.application.id}) + results = helper_test_http_method(client, "post", url, data, users) + assert results == [405, 405, 405] + + +def test_applications_retrieve_token(client, data): + url=reverse('applications-token', kwargs={"pk": data.token.application.id}) + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + results = helper_test_http_method(client, "get", url, None, users) + assert results == [401, 200, 200] + + +def test_application_tokens_retrieve(client, data): + url = reverse('application-tokens-detail', kwargs={"pk": data.token.id}) + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + results = helper_test_http_method(client, "get", url, None, users) + assert results == [401, 404, 200] + + +def test_application_tokens_authorize(client, data): + url=reverse('application-tokens-authorize') + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + data = json.dumps({ + "application": data.token.application.id, + "state": "random-state-123123", + }) + + results = helper_test_http_method(client, "post", url, data, users) + assert results == [401, 200, 200] + + +def test_application_tokens_validate(client, data): + url=reverse('application-tokens-validate') + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + data = json.dumps({ + "application": data.token.application.id, + "key": data.token.application.key, + "auth_code": data.token.auth_code, + "state": data.token.state + }) + + results = helper_test_http_method(client, "post", url, data, users) + assert results == [200, 200, 200] + + +def test_application_tokens_update(client, data): + url = reverse('application-tokens-detail', kwargs={"pk": data.token.id}) + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + patch_data = json.dumps({"application": data.token.application.id}) + results = helper_test_http_method(client, "patch", url, patch_data, users) + assert results == [405, 405, 405] + + +def test_application_tokens_delete(client, data): + url = reverse('application-tokens-detail', kwargs={"pk": data.token.id}) + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + results = helper_test_http_method(client, "delete", url, None, users) + assert results == [401, 403, 204] + + +def test_application_tokens_list(client, data): + url = reverse('application-tokens-list') + + users = [ + None, + data.registered_user, + data.registered_user_with_token + ] + + results = helper_test_http_method(client, "get", url, None, users) + assert results == [401, 200, 200] diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index e8e87048..4dcdfa7f 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index c6c99f2d..469efacc 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -9,6 +9,7 @@ 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.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher from taiga.projects.occ import OCCResourceMixin from unittest import mock @@ -160,6 +161,119 @@ def test_issue_update(client, data): assert results == [401, 403, 403, 200, 200] +def test_issue_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + issue_status1 = f.IssueStatusFactory.create(project=project1) + issue_status2 = f.IssueStatusFactory.create(project=project2) + + priority1 = f.PriorityFactory.create(project=project1) + priority2 = f.PriorityFactory.create(project=project2) + + severity1 = f.SeverityFactory.create(project=project1) + severity2 = f.SeverityFactory.create(project=project2) + + issue_type1 = f.IssueTypeFactory.create(project=project1) + issue_type2 = f.IssueTypeFactory.create(project=project2) + + project1.default_issue_status = issue_status1 + project2.default_issue_status = issue_status2 + + project1.default_priority = priority1 + project2.default_priority = priority2 + + project1.default_severity = severity1 + project2.default_severity = severity2 + + project1.default_issue_type = issue_type1 + project2.default_issue_type = issue_type2 + + project1.save() + project2.save() + + membership1 = f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership2 = f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership3 = f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership4 = f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + issue = f.IssueFactory.create(project=project1) + + url = reverse('issues-detail', kwargs={"pk": issue.pk}) + + # Test user with permissions in both projects + client.login(user1) + + issue_data = IssueSerializer(issue).data + issue_data["project"] = project2.id + issue_data = json.dumps(issue_data) + + response = client.put(url, data=issue_data, content_type="application/json") + + assert response.status_code == 200 + + issue.project = project1 + issue.save() + + # Test user with permissions in only origin project + client.login(user2) + + issue_data = IssueSerializer(issue).data + issue_data["project"] = project2.id + issue_data = json.dumps(issue_data) + + response = client.put(url, data=issue_data, content_type="application/json") + + assert response.status_code == 403 + + issue.project = project1 + issue.save() + + # Test user with permissions in only destionation project + client.login(user3) + + issue_data = IssueSerializer(issue).data + issue_data["project"] = project2.id + issue_data = json.dumps(issue_data) + + response = client.put(url, data=issue_data, content_type="application/json") + + assert response.status_code == 403 + + issue.project = project1 + issue.save() + + # Test user without permissions in the projects + client.login(user4) + + issue_data = IssueSerializer(issue).data + issue_data["project"] = project2.id + issue_data = json.dumps(issue_data) + + response = client.put(url, data=issue_data, content_type="application/json") + + assert response.status_code == 403 + + issue.project = project1 + issue.save() + + 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}) @@ -364,12 +478,10 @@ def test_issue_action_upvote(client, data): results = helper_test_http_method(client, 'post', public_url, "", users) assert results == [401, 200, 200, 200, 200] - results = helper_test_http_method(client, 'post', private_url1, "", users) assert results == [401, 200, 200, 200, 200] - results = helper_test_http_method(client, 'post', private_url2, "", users) - assert results == [401, 403, 403, 200, 200] + assert results == [404, 404, 404, 200, 200] def test_issue_action_downvote(client, data): @@ -387,18 +499,16 @@ def test_issue_action_downvote(client, data): results = helper_test_http_method(client, 'post', public_url, "", users) assert results == [401, 200, 200, 200, 200] - results = helper_test_http_method(client, 'post', private_url1, "", users) assert results == [401, 200, 200, 200, 200] - results = helper_test_http_method(client, 'post', private_url2, "", users) - assert results == [401, 403, 403, 200, 200] + assert results == [404, 404, 404, 200, 200] def test_issue_voters_list(client, data): - public_url = reverse('issue-voters-list', kwargs={"issue_id": data.public_issue.pk}) - private_url1 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue1.pk}) - private_url2 = reverse('issue-voters-list', kwargs={"issue_id": data.private_issue2.pk}) + 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}) users = [ None, @@ -410,21 +520,22 @@ def test_issue_voters_list(client, data): results = helper_test_http_method(client, 'get', public_url, None, users) assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url1, None, users) assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] def test_issue_voters_retrieve(client, data): add_vote(data.public_issue, data.project_owner) - public_url = reverse('issue-voters-detail', kwargs={"issue_id": data.public_issue.pk, "pk": data.project_owner.pk}) + public_url = reverse('issue-voters-detail', kwargs={"resource_id": data.public_issue.pk, + "pk": data.project_owner.pk}) add_vote(data.private_issue1, data.project_owner) - private_url1 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue1.pk, "pk": data.project_owner.pk}) + private_url1 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue1.pk, + "pk": data.project_owner.pk}) add_vote(data.private_issue2, data.project_owner) - private_url2 = reverse('issue-voters-detail', kwargs={"issue_id": data.private_issue2.pk, "pk": data.project_owner.pk}) + private_url2 = reverse('issue-voters-detail', kwargs={"resource_id": data.private_issue2.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -436,10 +547,8 @@ def test_issue_voters_retrieve(client, data): results = helper_test_http_method(client, 'get', public_url, None, users) assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url1, None, users) assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] @@ -466,3 +575,93 @@ 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] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_watchers_retrieve(client, data): + add_watcher(data.public_issue, data.project_owner) + public_url = reverse('issue-watchers-detail', kwargs={"resource_id": data.public_issue.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_issue1, data.project_owner) + private_url1 = reverse('issue-watchers-detail', kwargs={"resource_id": data.private_issue1.pk, + "pk": data.project_owner.pk}) + 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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index 955754f9..40a8c008 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone +from taiga.projects.notifications.services import add_watcher from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS from tests import factories as f @@ -274,3 +275,93 @@ 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] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_milestone_watchers_retrieve(client, data): + add_watcher(data.public_milestone, data.project_owner) + public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_milestone1, data.project_owner) + private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk, + "pk": data.project_owner.pk}) + 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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 0c97c952..e604ac7f 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -70,16 +70,16 @@ def data(): project_ct = ContentType.objects.get_for_model(Project) - f.VoteFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms) - f.VoteFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner) - f.VoteFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms) - f.VoteFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_owner) - f.VoteFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_member_with_perms) - f.VoteFactory(content_type=project_ct, object_id=m.private_project2.pk, user=m.project_owner) + f.LikeFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_member_with_perms) + f.LikeFactory(content_type=project_ct, object_id=m.public_project.pk, user=m.project_owner) + f.LikeFactory(content_type=project_ct, object_id=m.private_project1.pk, user=m.project_member_with_perms) + 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.VotesFactory(content_type=project_ct, object_id=m.public_project.pk, count=2) - f.VotesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2) - f.VotesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2) + f.LikesFactory(content_type=project_ct, object_id=m.public_project.pk, count=2) + f.LikesFactory(content_type=project_ct, object_id=m.private_project1.pk, count=2) + f.LikesFactory(content_type=project_ct, object_id=m.private_project2.pk, count=2) return m @@ -109,6 +109,7 @@ def test_project_update(client, data): project_data = ProjectDetailSerializer(data.private_project2).data project_data["is_private"] = False + project_data = json.dumps(project_data) users = [ @@ -198,44 +199,6 @@ def test_project_action_stats(client, data): assert results == [404, 404, 200, 200] -def test_project_action_star(client, data): - public_url = reverse('projects-star', kwargs={"pk": data.public_project.pk}) - private1_url = reverse('projects-star', kwargs={"pk": data.private_project1.pk}) - private2_url = reverse('projects-star', kwargs={"pk": data.private_project2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_with_perms, - data.project_owner - ] - results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 200, 200, 200] - results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 200, 200, 200] - results = helper_test_http_method(client, 'post', private2_url, None, users) - assert results == [404, 404, 200, 200] - - -def test_project_action_unstar(client, data): - public_url = reverse('projects-unstar', kwargs={"pk": data.public_project.pk}) - private1_url = reverse('projects-unstar', kwargs={"pk": data.private_project1.pk}) - private2_url = reverse('projects-unstar', kwargs={"pk": data.private_project2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_with_perms, - data.project_owner - ] - results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 200, 200, 200] - results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 200, 200, 200] - results = helper_test_http_method(client, 'post', private2_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}) @@ -255,10 +218,10 @@ def test_project_action_issues_stats(client, data): assert results == [404, 404, 200, 200] -def test_project_action_issues_filters_data(client, data): - public_url = reverse('projects-issue-filters-data', kwargs={"pk": data.public_project.pk}) - private1_url = reverse('projects-issue-filters-data', kwargs={"pk": data.private_project1.pk}) - private2_url = reverse('projects-issue-filters-data', kwargs={"pk": data.private_project2.pk}) +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}) users = [ None, @@ -266,18 +229,37 @@ def test_project_action_issues_filters_data(client, data): data.project_member_with_perms, data.project_owner ] - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private2_url, None, users) + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] -def test_project_action_fans(client, data): - public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk}) - private1_url = reverse('projects-fans', kwargs={"pk": data.private_project1.pk}) - private2_url = reverse('projects-fans', kwargs={"pk": data.private_project2.pk}) +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}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +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}) users = [ None, @@ -292,13 +274,16 @@ def test_project_action_fans(client, data): results = helper_test_http_method_and_count(client, 'get', private1_url, None, users) 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 == [(404, 1), (404, 1), (404, 1), (200, 2), (200, 2)] + assert results == [(401, 0), (403, 0), (403, 0), (200, 2), (200, 2)] -def test_user_action_starred(client, data): - url1 = reverse('users-starred', kwargs={"pk": data.project_member_without_perms.pk}) - url2 = reverse('users-starred', kwargs={"pk": data.project_member_with_perms.pk}) - url3 = reverse('users-starred', kwargs={"pk": data.project_owner.pk}) +def test_project_fans_retrieve(client, data): + public_url = reverse('project-fans-detail', kwargs={"resource_id": data.public_project.pk, + "pk": data.project_owner.pk}) + private1_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project1.pk, + "pk": data.project_owner.pk}) + private2_url = reverse('project-fans-detail', kwargs={"resource_id": data.private_project2.pk, + "pk": data.project_owner.pk}) users = [ None, @@ -308,12 +293,57 @@ def test_user_action_starred(client, data): data.project_owner ] - results = helper_test_http_method_and_count(client, 'get', url1, None, users) - assert results == [(200, 0), (200, 0), (200, 0), (200, 0), (200, 0)] - results = helper_test_http_method_and_count(client, 'get', url2, None, users) - assert results == [(200, 3), (200, 3), (200, 3), (200, 3), (200, 3)] - results = helper_test_http_method_and_count(client, 'get', url3, None, users) + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', public_url, None, users) + assert results == [(200, 1), (200, 1), (200, 1), (200, 1), (200, 1)] + results = helper_test_http_method_and_count(client, 'get', private1_url, None, users) 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)] + + +def test_project_watchers_retrieve(client, data): + public_url = reverse('project-watchers-detail', kwargs={"resource_id": data.public_project.pk, + "pk": data.project_owner.pk}) + private1_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project1.pk, + "pk": data.project_owner.pk}) + private2_url = reverse('project-watchers-detail', kwargs={"resource_id": data.private_project2.pk, + "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] def test_project_action_create_template(client, data): @@ -432,3 +462,41 @@ 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] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 200, 200, 200] + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 200, 200] diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index 0cb484a3..4f5f4f54 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -128,3 +128,21 @@ def test_resolver_list(client, data): "task": data.task.pk, "issue": data.issue.pk, "milestone": data.milestone.pk} + + response = client.json.get("{}?project={}&ref={}".format(url, + data.private_project2.slug, + data.us.ref)) + assert response.data == {"project": data.private_project2.pk, + "us": data.us.pk} + + response = client.json.get("{}?project={}&ref={}".format(url, + data.private_project2.slug, + data.task.ref)) + assert response.data == {"project": data.private_project2.pk, + "task": data.task.pk} + + response = client.json.get("{}?project={}&ref={}".format(url, + data.private_project2.slug, + data.issue.ref)) + assert response.data == {"project": data.private_project2.pk, + "issue": data.issue.pk} 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 773c44cb..da70bd4e 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 218b1e6d..4a871e8e 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -9,6 +9,8 @@ from taiga.projects.occ import OCCResourceMixin from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher from unittest import mock @@ -83,18 +85,25 @@ def data(): user=m.project_owner, is_owner=True) + milestone_public_task = f.MilestoneFactory(project=m.public_project) + milestone_private_task1 = f.MilestoneFactory(project=m.private_project1) + milestone_private_task2 = f.MilestoneFactory(project=m.private_project2) + m.public_task = f.TaskFactory(project=m.public_project, status__project=m.public_project, - milestone__project=m.public_project, - user_story__project=m.public_project) + milestone=milestone_public_task, + user_story__project=m.public_project, + user_story__milestone=milestone_public_task) m.private_task1 = f.TaskFactory(project=m.private_project1, status__project=m.private_project1, - milestone__project=m.private_project1, - user_story__project=m.private_project1) + milestone=milestone_private_task1, + user_story__project=m.private_project1, + user_story__milestone=milestone_private_task1) m.private_task2 = f.TaskFactory(project=m.private_project2, status__project=m.private_project2, - milestone__project=m.private_project2, - user_story__project=m.private_project2) + milestone=milestone_private_task2, + user_story__project=m.private_project2, + user_story__milestone=milestone_private_task2) m.public_project.default_task_status = m.public_task.status m.public_project.save() @@ -160,6 +169,101 @@ def test_task_update(client, data): assert results == [401, 403, 403, 200, 200] +def test_task_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + task_status1 = f.TaskStatusFactory.create(project=project1) + task_status2 = f.TaskStatusFactory.create(project=project2) + + project1.default_task_status = task_status1 + project2.default_task_status = task_status2 + + project1.save() + project2.save() + + membership1 = f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership2 = f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership3 = f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership4 = f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + task = f.TaskFactory.create(project=project1) + + url = reverse('tasks-detail', kwargs={"pk": task.pk}) + + # Test user with permissions in both projects + client.login(user1) + + task_data = TaskSerializer(task).data + task_data["project"] = project2.id + task_data = json.dumps(task_data) + + response = client.put(url, data=task_data, content_type="application/json") + + assert response.status_code == 200 + + task.project = project1 + task.save() + + # Test user with permissions in only origin project + client.login(user2) + + task_data = TaskSerializer(task).data + task_data["project"] = project2.id + task_data = json.dumps(task_data) + + response = client.put(url, data=task_data, content_type="application/json") + + assert response.status_code == 403 + + task.project = project1 + task.save() + + # Test user with permissions in only destionation project + client.login(user3) + + task_data = TaskSerializer(task).data + task_data["project"] = project2.id + task_data = json.dumps(task_data) + + response = client.put(url, data=task_data, content_type="application/json") + + assert response.status_code == 403 + + task.project = project1 + task.save() + + # Test user without permissions in the projects + client.login(user4) + + task_data = TaskSerializer(task).data + task_data["project"] = project2.id + task_data = json.dumps(task_data) + + response = client.put(url, data=task_data, content_type="application/json") + + assert response.status_code == 403 + + task.project = project1 + task.save() + + def test_task_delete(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) @@ -314,6 +418,96 @@ def test_task_action_bulk_create(client, data): assert results == [401, 403, 403, 200, 200] +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_voters_retrieve(client, data): + add_vote(data.public_task, data.project_owner) + public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_task1, data.project_owner) + private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_task2, data.project_owner) + private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk, + "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + def test_tasks_csv(client, data): url = reverse('tasks-csv') csv_public_uuid = data.public_project.tasks_csv_uuid @@ -336,3 +530,93 @@ 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] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_watchers_retrieve(client, data): + add_watcher(data.public_task, data.project_owner) + public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_task1, data.project_owner) + private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_task2, data.project_owner) + private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk, + "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py index fada3a72..a6604988 100644 --- a/tests/integration/resources_permissions/test_users_resources.py +++ b/tests/integration/resources_permissions/test_users_resources.py @@ -287,3 +287,39 @@ def test_user_action_change_email(client, data): after_each_request() results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request) assert results == [204, 204, 204] + + +def test_user_list_watched(client, data): + url = reverse('users-watched', kwargs={"pk": data.registered_user.pk}) + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200, 200] + + +def test_user_list_liked(client, data): + url = reverse('users-liked', kwargs={"pk": data.registered_user.pk}) + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200, 200] + + +def test_user_list_voted(client, data): + url = reverse('users-voted', kwargs={"pk": data.registered_user.pk}) + users = [ + None, + data.registered_user, + data.other_user, + data.superuser, + ] + results = helper_test_http_method(client, 'get', url, None, users) + assert results == [200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index 17a30bc8..766bae97 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 05a1ce4c..20881aed 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -9,6 +9,8 @@ from taiga.projects.occ import OCCResourceMixin from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher from unittest import mock @@ -89,13 +91,19 @@ def data(): m.public_role_points = f.RolePointsFactory(role=m.public_project.roles.all()[0], points=m.public_points, - user_story__project=m.public_project) + user_story__project=m.public_project, + user_story__milestone__project=m.public_project, + user_story__status__project=m.public_project) m.private_role_points1 = f.RolePointsFactory(role=m.private_project1.roles.all()[0], points=m.private_points1, - user_story__project=m.private_project1) + user_story__project=m.private_project1, + user_story__milestone__project=m.private_project1, + user_story__status__project=m.private_project1) m.private_role_points2 = f.RolePointsFactory(role=m.private_project2.roles.all()[0], points=m.private_points2, - user_story__project=m.private_project2) + user_story__project=m.private_project2, + user_story__milestone__project=m.private_project2, + user_story__status__project=m.private_project2) m.public_user_story = m.public_role_points.user_story m.private_user_story1 = m.private_role_points1.user_story @@ -158,6 +166,101 @@ def test_user_story_update(client, data): assert results == [401, 403, 403, 200, 200] +def test_user_story_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + us_status1 = f.UserStoryStatusFactory.create(project=project1) + us_status2 = f.UserStoryStatusFactory.create(project=project2) + + project1.default_us_status = us_status1 + project2.default_us_status = us_status2 + + project1.save() + project2.save() + + membership1 = f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership2 = f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership3 = f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership4 = f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + us = f.UserStoryFactory.create(project=project1) + + url = reverse('userstories-detail', kwargs={"pk": us.pk}) + + # Test user with permissions in both projects + client.login(user1) + + us_data = UserStorySerializer(us).data + us_data["project"] = project2.id + us_data = json.dumps(us_data) + + response = client.put(url, data=us_data, content_type="application/json") + + assert response.status_code == 200 + + us.project = project1 + us.save() + + # Test user with permissions in only origin project + client.login(user2) + + us_data = UserStorySerializer(us).data + us_data["project"] = project2.id + us_data = json.dumps(us_data) + + response = client.put(url, data=us_data, content_type="application/json") + + assert response.status_code == 403 + + us.project = project1 + us.save() + + # Test user with permissions in only destionation project + client.login(user3) + + us_data = UserStorySerializer(us).data + us_data["project"] = project2.id + us_data = json.dumps(us_data) + + response = client.put(url, data=us_data, content_type="application/json") + + assert response.status_code == 403 + + us.project = project1 + us.save() + + # Test user without permissions in the projects + client.login(user4) + + us_data = UserStorySerializer(us).data + us_data["project"] = project2.id + us_data = json.dumps(us_data) + + response = client.put(url, data=us_data, content_type="application/json") + + assert response.status_code == 403 + + us.project = project1 + us.save() + + 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}) @@ -314,6 +417,95 @@ 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] +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_voters_retrieve(client, data): + add_vote(data.public_user_story, data.project_owner) + public_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.public_user_story.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_user_story1, data.project_owner) + private_url1 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story1.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_user_story2, data.project_owner) + private_url2 = reverse('userstory-voters-detail', kwargs={"resource_id": data.private_user_story2.pk, + "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + def test_user_stories_csv(client, data): url = reverse('userstories-csv') @@ -337,3 +529,93 @@ def test_user_stories_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] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_userstory_watchers_retrieve(client, data): + add_watcher(data.public_user_story, data.project_owner) + public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_user_story1, data.project_owner) + private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk, + "pk": data.project_owner.pk}) + 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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index cf6089b7..14f2f92b 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -1,10 +1,11 @@ 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.notifications.services import add_watcher +from taiga.projects.occ import OCCResourceMixin from taiga.projects.wiki.serializers import WikiPageSerializer, WikiLinkSerializer from taiga.projects.wiki.models import WikiPage, WikiLink -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS -from taiga.projects.occ import OCCResourceMixin from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -436,3 +437,93 @@ def test_wiki_link_patch(client, data): patch_data = json.dumps({"title": "test"}) results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) assert results == [401, 403, 403, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + + +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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wikipage_watchers_retrieve(client, data): + add_watcher(data.public_wiki_page, data.project_owner) + public_url = reverse('wiki-watchers-detail', kwargs={"resource_id": data.public_wiki_page.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_wiki_page1, data.project_owner) + private_url1 = reverse('wiki-watchers-detail', kwargs={"resource_id": data.private_wiki_page1.pk, + "pk": data.project_owner.pk}) + 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}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/test_application_tokens.py b/tests/integration/test_application_tokens.py new file mode 100644 index 00000000..da6d986e --- /dev/null +++ b/tests/integration/test_application_tokens.py @@ -0,0 +1,102 @@ +from django.core.urlresolvers import reverse + +from taiga.external_apps import encryption +from taiga.external_apps import models + + +from .. import factories as f + +import json +import pytest +pytestmark = pytest.mark.django_db + + +def test_own_tokens_listing(client): + user_1 = f.UserFactory.create() + user_2 = f.UserFactory.create() + token_1 = f.ApplicationTokenFactory(user=user_1) + token_2 = f.ApplicationTokenFactory(user=user_2) + url = reverse("application-tokens-list") + client.login(user_1) + response = client.json.get(url) + assert response.status_code == 200 + assert len(response.data) == 1 + assert response.data[0].get("id") == token_1.id + assert response.data[0].get("application").get("id") == token_1.application.id + + +def test_retrieve_existing_token_for_application(client): + token = f.ApplicationTokenFactory() + url = reverse("applications-token", args=[token.application.id]) + client.login(token.user) + response = client.json.get(url) + assert response.status_code == 200 + assert response.data.get("application").get("id") == token.application.id + + + +def test_retrieve_unexisting_token_for_application(client): + user = f.UserFactory.create() + url = reverse("applications-token", args=[-1]) + client.login(user) + response = client.json.get(url) + assert response.status_code == 404 + + +def test_token_authorize(client): + user = f.UserFactory.create() + application = f.ApplicationFactory() + url = reverse("application-tokens-authorize") + client.login(user) + + data = json.dumps({ + "application": application.id, + "state": "random-state" + }) + + response = client.json.post(url, data) + + assert response.status_code == 200 + assert response.data["state"] == "random-state" + auth_code_1 = response.data["auth_code"] + + response = client.json.post(url, data) + assert response.status_code == 200 + assert response.data["state"] == "random-state" + auth_code_2 = response.data["auth_code"] + assert auth_code_1 != auth_code_2 + + +def test_token_authorize_invalid_app(client): + user = f.UserFactory.create() + url = reverse("application-tokens-authorize") + client.login(user) + + data = json.dumps({ + "application": 33, + "state": "random-state" + }) + + response = client.json.post(url, data) + assert response.status_code == 404 + + +def test_token_validate(client): + user = f.UserFactory.create() + application = f.ApplicationFactory(next_url="http://next.url") + token = f.ApplicationTokenFactory(auth_code="test-auth-code", state="test-state", application=application) + url = reverse("application-tokens-validate") + client.login(user) + + data = { + "application": token.application.id, + "auth_code": "test-auth-code", + "state": "test-state" + } + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + + token = models.ApplicationToken.objects.get(id=token.id) + decyphered_token = encryption.decrypt(response.data["cyphered_token"], token.application.key)[0] + decyphered_token = json.loads(decyphered_token.decode("utf-8")) + assert decyphered_token["token"] == token.token diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index e776b569..e7a7805a 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 65152aa5..a29c5d45 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index fee38830..6ffaf3b5 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index 6e602269..4b1f5079 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -1,6 +1,6 @@ -# Copyright (C) 2015 Andrey Antukh -# Copyright (C) 2015 Jesús Espino -# Copyright (C) 2015 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py index 7758fdf6..69f58c32 100644 --- a/tests/integration/test_exporter_api.py +++ b/tests/integration/test_exporter_api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -47,7 +47,7 @@ def test_valid_project_export_with_celery_disabled(client, settings): response = client.get(url, content_type="application/json") assert response.status_code == 200 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert "url" in response_data @@ -63,7 +63,7 @@ def test_valid_project_export_with_celery_enabled(client, settings): response = client.get(url, content_type="application/json") assert response.status_code == 202 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert "export_id" in response_data diff --git a/tests/integration/test_fan_projects.py b/tests/integration/test_fan_projects.py new file mode 100644 index 00000000..6f60e50b --- /dev/null +++ b/tests/integration/test_fan_projects.py @@ -0,0 +1,123 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# Copyright (C) 2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("projects-like", args=(project.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("projects-unlike", args=(project.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.LikeFactory.create(content_object=project, user=user) + url = reverse("project-fans-list", args=(project.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + like = f.LikeFactory.create(content_object=project, user=user) + url = reverse("project-fans-detail", args=(project.id, like.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == like.user.id + + +def test_get_project_total_fans(client): + user = f.UserFactory.create() + project = f.create_project(owner=user) + f.MembershipFactory.create(project=project, user=user, is_owner=True) + url = reverse("projects-detail", args=(project.id,)) + + f.LikesFactory.create(content_object=project, count=5) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['total_fans'] == 5 + + +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.LikesFactory.create(content_object=project) + url_detail = reverse("projects-detail", args=(project.id,)) + url_like = reverse("projects-like", args=(project.id,)) + url_unlike = reverse("projects-unlike", args=(project.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_fans'] == 0 + assert response.data['is_fan'] == False + + response = client.post(url_like) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_fans'] == 1 + assert response.data['is_fan'] == True + + response = client.post(url_unlike) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_fans'] == 0 + assert response.data['is_fan'] == False diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index 020eadcd..1b5c4f48 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -199,7 +199,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) + us = f.create_userstory(project=project, status__project=project) f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) qs_all = HistoryEntry.objects.all() @@ -213,7 +213,7 @@ def test_history_with_only_comment_shouldnot_be_hidden(client): client.login(project.owner) response = client.patch(url, data, content_type="application/json") - assert response.status_code == 200, response.content + assert response.status_code == 200, str(response.content) assert qs_all.count() == 1 assert qs_hidden.count() == 0 diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 5f5357fb..bef9330f 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -265,7 +265,7 @@ def test_issues_event_opened_issue(client): issue.project.save() Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True) notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project) - notify_policy.notify_level = NotifyLevel.watch + notify_policy.notify_level = NotifyLevel.all notify_policy.save() payload = { @@ -476,7 +476,7 @@ def test_api_get_project_modules(client): client.login(project.owner) response = client.get(url) assert response.status_code == 200 - content = json.loads(response.content.decode("utf-8")) + content = response.data assert "bitbucket" in content assert content["bitbucket"]["secret"] != "" assert content["bitbucket"]["webhooks_url"] != "" diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 43b092f5..5b832643 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -30,7 +30,7 @@ def test_bad_signature(client): response = client.post(url, json.dumps(data), HTTP_X_HUB_SIGNATURE="sha1=badbadbad", content_type="application/json") - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert response.status_code == 400 assert "Bad signature" in response_content["_error_message"] @@ -243,7 +243,7 @@ def test_issues_event_opened_issue(client): issue.project.save() Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True) notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project) - notify_policy.notify_level = NotifyLevel.watch + notify_policy.notify_level = NotifyLevel.all notify_policy.save() payload = { @@ -445,7 +445,7 @@ def test_api_get_project_modules(client): client.login(project.owner) response = client.get(url) assert response.status_code == 200 - content = json.loads(response.content.decode("utf-8")) + content = response.data assert "github" in content assert content["github"]["secret"] != "" assert content["github"]["webhooks_url"] != "" diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index 6849ca49..7264b2c1 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -13,6 +13,7 @@ from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory from taiga.projects.models import Membership +from taiga.projects.history.services import get_history_queryset_by_model_instance, take_snapshot from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.models import NotifyPolicy from taiga.projects import services @@ -33,7 +34,7 @@ def test_bad_signature(client): url = "{}?project={}&key={}".format(url, project.id, "badbadbad") data = {} response = client.post(url, json.dumps(data), content_type="application/json") - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert response.status_code == 400 assert "Bad signature" in response_content["_error_message"] @@ -308,7 +309,7 @@ def test_issues_event_opened_issue(client): issue.project.save() Membership.objects.create(user=issue.owner, project=issue.project, role=f.RoleFactory.create(project=issue.project), is_owner=True) notify_policy = NotifyPolicy.objects.get(user=issue.owner, project=issue.project) - notify_policy.notify_level = NotifyLevel.watch + notify_policy.notify_level = NotifyLevel.all notify_policy.save() payload = { @@ -384,6 +385,134 @@ def test_issues_event_bad_issue(client): assert len(mail.outbox) == 0 +def test_issue_comment_event_on_existing_issue_task_and_us(client): + project = f.ProjectFactory() + role = f.RoleFactory(project=project, permissions=["view_tasks", "view_issues", "view_us"]) + f.MembershipFactory(project=project, role=role, user=project.owner) + user = f.UserFactory() + + issue = f.IssueFactory.create(external_reference=["gitlab", "http://gitlab.com/test/project/issues/11"], owner=project.owner, project=project) + take_snapshot(issue, user=user) + task = f.TaskFactory.create(external_reference=["gitlab", "http://gitlab.com/test/project/issues/11"], owner=project.owner, project=project) + take_snapshot(task, user=user) + us = f.UserStoryFactory.create(external_reference=["gitlab", "http://gitlab.com/test/project/issues/11"], owner=project.owner, project=project) + take_snapshot(us, user=user) + + payload = { + "user": { + "username": "test" + }, + "issue": { + "iid": "11", + "title": "test-title", + }, + "object_attributes": { + "noteable_type": "Issue", + "note": "Test body", + }, + "repository": { + "homepage": "http://gitlab.com/test/project", + }, + } + + + mail.outbox = [] + + assert get_history_queryset_by_model_instance(issue).count() == 0 + assert get_history_queryset_by_model_instance(task).count() == 0 + assert get_history_queryset_by_model_instance(us).count() == 0 + + ev_hook = event_hooks.IssueCommentEventHook(issue.project, payload) + ev_hook.process_event() + + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert "Test body" in issue_history[0].comment + + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert "Test body" in issue_history[0].comment + + us_history = get_history_queryset_by_model_instance(us) + assert us_history.count() == 1 + assert "Test body" in issue_history[0].comment + + assert len(mail.outbox) == 3 + + +def test_issue_comment_event_on_not_existing_issue_task_and_us(client): + issue = f.IssueFactory.create(external_reference=["gitlab", "10"]) + take_snapshot(issue, user=issue.owner) + task = f.TaskFactory.create(project=issue.project, external_reference=["gitlab", "10"]) + take_snapshot(task, user=task.owner) + us = f.UserStoryFactory.create(project=issue.project, external_reference=["gitlab", "10"]) + take_snapshot(us, user=us.owner) + + payload = { + "user": { + "username": "test" + }, + "issue": { + "iid": "99999", + "title": "test-title", + }, + "object_attributes": { + "noteable_type": "Issue", + "note": "test comment", + }, + "repository": { + "homepage": "test", + }, + } + + mail.outbox = [] + + assert get_history_queryset_by_model_instance(issue).count() == 0 + assert get_history_queryset_by_model_instance(task).count() == 0 + assert get_history_queryset_by_model_instance(us).count() == 0 + + ev_hook = event_hooks.IssueCommentEventHook(issue.project, payload) + ev_hook.process_event() + + assert get_history_queryset_by_model_instance(issue).count() == 0 + assert get_history_queryset_by_model_instance(task).count() == 0 + assert get_history_queryset_by_model_instance(us).count() == 0 + + assert len(mail.outbox) == 0 + + +def test_issues_event_bad_comment(client): + issue = f.IssueFactory.create(external_reference=["gitlab", "10"]) + take_snapshot(issue, user=issue.owner) + + payload = { + "user": { + "username": "test" + }, + "issue": { + "iid": "10", + "title": "test-title", + }, + "object_attributes": { + "noteable_type": "Issue", + }, + "repository": { + "homepage": "test", + }, + } + ev_hook = event_hooks.IssueCommentEventHook(issue.project, payload) + + mail.outbox = [] + + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "Invalid issue comment information" + + assert Issue.objects.count() == 1 + assert len(mail.outbox) == 0 + + def test_api_get_project_modules(client): project = f.create_project() f.MembershipFactory(project=project, user=project.owner, is_owner=True) @@ -393,7 +522,7 @@ def test_api_get_project_modules(client): client.login(project.owner) response = client.get(url) assert response.status_code == 200 - content = json.loads(response.content.decode("utf-8")) + content = response.data assert "gitlab" in content assert content["gitlab"]["secret"] != "" assert content["gitlab"]["webhooks_url"] != "" diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index d5f07493..aa588bec 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -47,18 +47,20 @@ def test_invalid_project_import(client): def test_valid_project_import_without_extra_data(client): user = f.UserFactory.create() + user_watching = f.UserFactory.create(email="testing@taiga.io") client.login(user) url = reverse("importer-list") data = { "name": "Imported project", "description": "Imported project", - "roles": [{"name": "Role"}] + "roles": [{"name": "Role"}], + "watchers": ["testing@taiga.io"] } response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data must_empty_children = [ "issues", "user_stories", "us_statuses", "wiki_pages", "priorities", "severities", "milestones", "points", "issue_types", "task_statuses", @@ -66,6 +68,7 @@ def test_valid_project_import_without_extra_data(client): ] assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children)) assert response_data["owner"] == user.email + assert response_data["watchers"] == [user.email, user_watching.email] def test_valid_project_import_with_not_existing_memberships(client): @@ -85,7 +88,7 @@ def test_valid_project_import_with_not_existing_memberships(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data # The new membership and the owner membership assert len(response_data["memberships"]) == 2 @@ -108,7 +111,7 @@ def test_valid_project_import_with_membership_uuid_rewrite(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert Membership.objects.filter(email="with-uuid@email.com", token="123").count() == 0 @@ -149,7 +152,7 @@ def test_valid_project_import_with_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data must_empty_children = [ "issues", "user_stories", "wiki_pages", "milestones", "wiki_links", @@ -178,7 +181,7 @@ def test_invalid_project_import_without_roles(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 2 assert Project.objects.filter(slug="imported-project").count() == 0 @@ -205,7 +208,7 @@ def test_invalid_project_import_with_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 7 assert Project.objects.filter(slug="imported-project").count() == 0 @@ -302,7 +305,7 @@ def test_valid_user_story_import(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["subject"] == "Imported issue" assert response_data["finish_date"] == "2014-10-24T00:00:00+0000" @@ -349,7 +352,7 @@ def test_valid_issue_import_without_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["owner"] == user.email assert response_data["ref"] is not None @@ -383,6 +386,7 @@ def test_valid_issue_import_with_custom_attributes_values(client): 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) project.default_issue_type = f.IssueTypeFactory.create(project=project) @@ -403,16 +407,18 @@ def test_valid_issue_import_with_extra_data(client): "name": "imported attachment", "data": base64.b64encode(b"TEST").decode("utf-8") } - }] + }], + "watchers": ["testing@taiga.io"] } response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data["attachments"]) == 1 assert response_data["owner"] == user.email assert response_data["ref"] is not None assert response_data["finished_date"] == "2014-10-24T00:00:00+0000" + assert response_data["watchers"] == [user_watching.email] def test_invalid_issue_import_with_extra_data(client): @@ -435,7 +441,7 @@ def test_invalid_issue_import_with_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 assert Issue.objects.filter(subject="Imported issue").count() == 0 @@ -460,7 +466,7 @@ def test_invalid_issue_import_with_bad_choices(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 url = reverse("importer-issue", args=[project.pk]) @@ -472,7 +478,7 @@ def test_invalid_issue_import_with_bad_choices(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 url = reverse("importer-issue", args=[project.pk]) @@ -484,7 +490,7 @@ def test_invalid_issue_import_with_bad_choices(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 url = reverse("importer-issue", args=[project.pk]) @@ -496,7 +502,7 @@ def test_invalid_issue_import_with_bad_choices(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 @@ -528,13 +534,14 @@ def test_valid_us_import_without_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["owner"] == user.email assert response_data["ref"] is not None 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) project.default_us_status = f.UserStoryStatusFactory.create(project=project) @@ -551,15 +558,17 @@ def test_valid_us_import_with_extra_data(client): "name": "imported attachment", "data": base64.b64encode(b"TEST").decode("utf-8") } - }] + }], + "watchers": ["testing@taiga.io"] } response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data["attachments"]) == 1 assert response_data["owner"] == user.email assert response_data["ref"] is not None + assert response_data["watchers"] == [user_watching.email] def test_invalid_us_import_with_extra_data(client): @@ -579,7 +588,7 @@ def test_invalid_us_import_with_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 assert UserStory.objects.filter(subject="Imported us").count() == 0 @@ -601,7 +610,7 @@ def test_invalid_us_import_with_bad_choices(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 @@ -633,7 +642,7 @@ def test_valid_task_import_without_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["owner"] == user.email assert response_data["ref"] is not None @@ -664,6 +673,7 @@ def test_valid_task_import_with_custom_attributes_values(client): 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) project.default_task_status = f.TaskStatusFactory.create(project=project) @@ -680,15 +690,17 @@ def test_valid_task_import_with_extra_data(client): "name": "imported attachment", "data": base64.b64encode(b"TEST").decode("utf-8") } - }] + }], + "watchers": ["testing@taiga.io"] } response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data["attachments"]) == 1 assert response_data["owner"] == user.email assert response_data["ref"] is not None + assert response_data["watchers"] == [user_watching.email] def test_invalid_task_import_with_extra_data(client): @@ -708,7 +720,7 @@ def test_invalid_task_import_with_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 assert Task.objects.filter(subject="Imported task").count() == 0 @@ -730,7 +742,7 @@ def test_invalid_task_import_with_bad_choices(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 @@ -781,12 +793,13 @@ def test_valid_wiki_page_import_without_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["owner"] == user.email 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) client.login(user) @@ -801,14 +814,16 @@ def test_valid_wiki_page_import_with_extra_data(client): "name": "imported attachment", "data": base64.b64encode(b"TEST").decode("utf-8") } - }] + }], + "watchers": ["testing@taiga.io"] } response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data["attachments"]) == 1 assert response_data["owner"] == user.email + assert response_data["watchers"] == [user_watching.email] def test_invalid_wiki_page_import_with_extra_data(client): @@ -826,7 +841,7 @@ def test_invalid_wiki_page_import_with_extra_data(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert len(response_data) == 1 assert WikiPage.objects.filter(slug="imported-wiki-page").count() == 0 @@ -858,7 +873,7 @@ def test_valid_wiki_link_import(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - json.loads(response.content.decode("utf-8")) + response.data @@ -877,6 +892,7 @@ def test_invalid_milestone_import(client): 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) client.login(user) @@ -886,12 +902,12 @@ def test_valid_milestone_import(client): "name": "Imported milestone", "estimated_start": "2014-10-10", "estimated_finish": "2014-10-20", + "watchers": ["testing@taiga.io"] } response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 201 - json.loads(response.content.decode("utf-8")) - + assert response.data["watchers"] == [user_watching.email] def test_milestone_import_duplicated_milestone(client): user = f.UserFactory.create() @@ -909,7 +925,7 @@ def test_milestone_import_duplicated_milestone(client): response = client.post(url, json.dumps(data), content_type="application/json") response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project" @@ -924,7 +940,7 @@ def test_invalid_dump_import(client): response = client.post(url, {'dump': data}) assert response.status_code == 400 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["_error_message"] == "Invalid dump format" @@ -945,7 +961,7 @@ def test_valid_dump_import_with_celery_disabled(client, settings): response = client.post(url, {'dump': data}) assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert "id" in response_data assert response_data["name"] == "Valid project" @@ -967,7 +983,7 @@ def test_valid_dump_import_with_celery_enabled(client, settings): response = client.post(url, {'dump': data}) assert response.status_code == 202 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert "import_id" in response_data @@ -987,7 +1003,7 @@ def test_dump_import_duplicated_project(client): response = client.post(url, {'dump': data}) assert response.status_code == 201 - response_data = json.loads(response.content.decode("utf-8")) + response_data = response.data assert response_data["name"] == "Test import" assert response_data["slug"] == "{}-test-import".format(user.username) diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index a3aa9db0..54722c93 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -189,6 +189,198 @@ def test_api_filter_by_text_6(client): assert response.status_code == 200 assert number_of_issues == 1 +def test_api_filters_data(client): + project = f.ProjectFactory.create() + user1 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user1, project=project) + user2 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user2, project=project) + user3 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user3, project=project) + + status0 = f.IssueStatusFactory.create(project=project) + status1 = f.IssueStatusFactory.create(project=project) + status2 = f.IssueStatusFactory.create(project=project) + status3 = f.IssueStatusFactory.create(project=project) + + type1 = f.IssueTypeFactory.create(project=project) + type2 = f.IssueTypeFactory.create(project=project) + + severity0 = f.SeverityFactory.create(project=project) + severity1 = f.SeverityFactory.create(project=project) + severity2 = f.SeverityFactory.create(project=project) + severity3 = f.SeverityFactory.create(project=project) + + priority0 = f.PriorityFactory.create(project=project) + priority1 = f.PriorityFactory.create(project=project) + priority2 = f.PriorityFactory.create(project=project) + priority3 = f.PriorityFactory.create(project=project) + + tag0 = "test1test2test3" + tag1 = "test1" + tag2 = "test2" + tag3 = "test3" + + # ------------------------------------------------------------------------------------------------ + # | Issue | Owner | Assigned To | Status | Type | Priority | Severity | Tags | + # |-------#--------#-------------#---------#-------#-----------#-----------#---------------------| + # | 0 | user2 | None | status3 | type1 | priority2 | severity1 | tag1 | + # | 1 | user1 | None | status3 | type2 | priority2 | severity1 | tag2 | + # | 2 | user3 | None | status1 | type1 | priority3 | severity2 | tag1 tag2 | + # | 3 | user2 | None | status0 | type2 | priority3 | severity1 | tag3 | + # | 4 | user1 | user1 | status0 | type1 | priority2 | severity3 | tag1 tag2 tag3 | + # | 5 | user3 | user1 | status2 | type2 | priority3 | severity2 | tag3 | + # | 6 | user2 | user1 | status3 | type1 | priority2 | severity0 | tag1 tag2 | + # | 7 | user1 | user2 | status0 | type2 | priority1 | severity3 | tag3 | + # | 8 | user3 | user2 | status3 | type1 | priority0 | severity1 | tag1 | + # | 9 | user2 | user3 | status1 | type2 | priority0 | severity2 | tag0 | + # ------------------------------------------------------------------------------------------------ + + issue0 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, type=type1, priority=priority2, severity=severity1, + tags=[tag1]) + issue1 = f.IssueFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, type=type2, priority=priority2, severity=severity1, + tags=[tag2]) + issue2 = f.IssueFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, type=type1, priority=priority3, severity=severity2, + tags=[tag1, tag2]) + issue3 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, type=type2, priority=priority3, severity=severity1, + tags=[tag3]) + issue4 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, type=type1, priority=priority2, severity=severity3, + tags=[tag1, tag2, tag3]) + issue5 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, type=type2, priority=priority3, severity=severity2, + tags=[tag3]) + issue6 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, type=type1, priority=priority2, severity=severity0, + tags=[tag1, tag2]) + issue7 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, type=type2, priority=priority1, severity=severity3, + tags=[tag3]) + issue8 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, type=type1, priority=priority0, severity=severity1, + tags=[tag1]) + issue9 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, type=type2, priority=priority0, severity=severity2, + tags=[tag0]) + + url = reverse("issues-filters-data") + "?project={}".format(project.id) + + client.login(user1) + + ## No filter + response = client.get(url) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['id'] == type1.id, response.data["types"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == type2.id, response.data["types"]))["count"] == 5 + + assert next(filter(lambda i: i['id'] == priority0.id, response.data["priorities"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == priority1.id, response.data["priorities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == priority2.id, response.data["priorities"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == priority3.id, response.data["priorities"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == severity0.id, response.data["severities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == severity1.id, response.data["severities"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 2 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 5 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + + ## Filter ((status0 or status3) and type1) + response = client.get(url + "&status={},{}&type={}".format(status3.id, status0.id, type1.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == type1.id, response.data["types"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == type2.id, response.data["types"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == priority0.id, response.data["priorities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == priority1.id, response.data["priorities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == priority2.id, response.data["priorities"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == priority3.id, response.data["priorities"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == severity0.id, response.data["severities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == severity1.id, response.data["severities"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + ## Filter ((tag1 and tag2) and (user1 or user2)) + response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == type1.id, response.data["types"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == type2.id, response.data["types"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == priority0.id, response.data["priorities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == priority1.id, response.data["priorities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == priority2.id, response.data["priorities"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == priority3.id, response.data["priorities"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == severity0.id, response.data["severities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == severity1.id, response.data["severities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + def test_get_invalid_csv(client): url = reverse("issues-csv") @@ -220,6 +412,6 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[16] == attr.name + assert row[18] == attr.name row = next(reader) - assert row[16] == "val1" + assert row[18] == "val1" diff --git a/tests/integration/test_mdrender.py b/tests/integration/test_mdrender.py index 3735eac2..cd075845 100644 --- a/tests/integration/test_mdrender.py +++ b/tests/integration/test_mdrender.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -47,6 +47,12 @@ def test_render_and_extract_mentions(): (_, extracted) = render_and_extract(dummy_project, "**@user1**") assert extracted['mentions'] == [user] +def test_render_and_extract_mentions_with_capitalized_username(): + user = factories.UserFactory(username="User1", full_name="test") + (_, extracted) = render_and_extract(dummy_project, "**@User1**") + assert extracted['mentions'] == [user] + + def test_proccessor_valid_email(): result = render(dummy_project, "**beta.tester@taiga.io**") expected_result = "

beta.tester@taiga.io

" diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index 1b3410a1..d5675399 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -47,3 +47,36 @@ def test_update_milestone_with_userstories_list(client): client.login(user) response = client.json.patch(url, json.dumps(form_data)) assert response.status_code == 200 + + +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.MilestoneFactory.create(project=project, owner=user, closed=True) + f.MilestoneFactory.create(project=project, owner=user, closed=True) + f.MilestoneFactory.create(project=project, owner=user, closed=True) + f.MilestoneFactory.create(project=project, owner=user, closed=False) + f.MilestoneFactory.create(owner=user, closed=False) + + url = reverse("milestones-list") + + client.login(project.owner) + response1 = client.json.get(url) + response2 = client.json.get(url, {"project": project.id}) + + assert response1.status_code == 200 + assert "taiga-info-total-closed-milestones" in response1["access-control-expose-headers"] + assert "taiga-info-total-opened-milestones" in response1["access-control-expose-headers"] + assert response1.has_header("Taiga-Info-Total-Closed-Milestones") == False + assert response1.has_header("Taiga-Info-Total-Opened-Milestones") == False + + assert response2.status_code == 200 + assert "taiga-info-total-closed-milestones" in response2["access-control-expose-headers"] + assert "taiga-info-total-opened-milestones" in response2["access-control-expose-headers"] + assert response2.has_header("Taiga-Info-Total-Closed-Milestones") == True + assert response2.has_header("Taiga-Info-Total-Opened-Milestones") == True + assert response2["taiga-info-total-closed-milestones"] == "3" + assert response2["taiga-info-total-opened-milestones"] == "1" diff --git a/tests/integration/test_neighbors.py b/tests/integration/test_neighbors.py index 33ee8d06..92a532dd 100644 --- a/tests/integration/test_neighbors.py +++ b/tests/integration/test_neighbors.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 7da6469d..3173874f 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,6 +17,13 @@ import pytest import time +import math +import base64 +import datetime +import hashlib +import binascii +import struct + from unittest.mock import MagicMock, patch from django.core.urlresolvers import reverse @@ -25,6 +32,7 @@ from .. import factories as f from taiga.base.utils import json from taiga.projects.notifications import services +from taiga.projects.notifications import utils from taiga.projects.notifications import models from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.history.choices import HistoryType @@ -32,6 +40,7 @@ from taiga.projects.history.services import take_snapshot from taiga.projects.issues.serializers import IssueSerializer from taiga.projects.userstories.serializers import UserStorySerializer from taiga.projects.tasks.serializers import TaskSerializer +from taiga.permissions.permissions import MEMBERS_PERMISSIONS pytestmark = pytest.mark.django_db @@ -43,22 +52,22 @@ def mail(): return mail -def test_attach_notify_policy_to_project_queryset(): +def test_attach_notify_level_to_project_queryset(): project1 = f.ProjectFactory.create() f.ProjectFactory.create() qs = project1.__class__.objects.order_by("id") - qs = services.attach_notify_policy_to_project_queryset(project1.owner, qs) + qs = utils.attach_notify_level_to_project_queryset(qs, project1.owner) assert len(qs) == 2 - assert qs[0].notify_level == NotifyLevel.notwatch - assert qs[1].notify_level == NotifyLevel.notwatch + assert qs[0].notify_level == NotifyLevel.involved + assert qs[1].notify_level == NotifyLevel.involved - services.create_notify_policy(project1, project1.owner, NotifyLevel.watch) + services.create_notify_policy(project1, project1.owner, NotifyLevel.all) qs = project1.__class__.objects.order_by("id") - qs = services.attach_notify_policy_to_project_queryset(project1.owner, qs) - assert qs[0].notify_level == NotifyLevel.watch - assert qs[1].notify_level == NotifyLevel.notwatch + qs = utils.attach_notify_level_to_project_queryset(qs, project1.owner) + assert qs[0].notify_level == NotifyLevel.all + assert qs[1].notify_level == NotifyLevel.involved def test_create_retrieve_notify_policy(): @@ -72,14 +81,14 @@ def test_create_retrieve_notify_policy(): current_number = policy_model_cls.objects.all().count() assert current_number == 1 - assert policy.notify_level == NotifyLevel.notwatch + assert policy.notify_level == NotifyLevel.involved def test_notify_policy_existence(): project = f.ProjectFactory.create() assert not services.notify_policy_exists(project, project.owner) - services.create_notify_policy(project, project.owner, NotifyLevel.watch) + services.create_notify_policy(project, project.owner, NotifyLevel.all) assert services.notify_policy_exists(project, project.owner) @@ -95,8 +104,38 @@ def test_analize_object_for_watchers(): history = MagicMock() history.comment = "" - services.analize_object_for_watchers(issue, history) - assert issue.watchers.add.call_count == 2 + services.analize_object_for_watchers(issue, history.comment, history.owner) + assert issue.add_watcher.call_count == 2 + + +def test_analize_object_for_watchers_adding_owner_non_empty_comment(): + user1 = f.UserFactory.create() + + issue = MagicMock() + issue.description = "Foo" + issue.content = "" + + history = MagicMock() + history.comment = "Comment" + history.owner = user1 + + services.analize_object_for_watchers(issue, history.comment, history.owner) + assert issue.add_watcher.call_count == 1 + + +def test_analize_object_for_watchers_no_adding_owner_empty_comment(): + user1 = f.UserFactory.create() + + issue = MagicMock() + issue.description = "Foo" + issue.content = "" + + history = MagicMock() + history.comment = "" + history.owner = user1 + + services.analize_object_for_watchers(issue, history.comment, history.owner) + assert issue.add_watcher.call_count == 0 def test_users_to_notify(): @@ -105,10 +144,25 @@ def test_users_to_notify(): role2 = f.RoleFactory.create(project=project, permissions=[]) member1 = f.MembershipFactory.create(project=project, role=role1) + policy_member1 = member1.user.notify_policies.get(project=project) + policy_member1.notify_level = NotifyLevel.none + policy_member1.save() member2 = f.MembershipFactory.create(project=project, role=role1) + policy_member2 = member2.user.notify_policies.get(project=project) + policy_member2.notify_level = NotifyLevel.none + policy_member2.save() member3 = f.MembershipFactory.create(project=project, role=role1) + policy_member3 = member3.user.notify_policies.get(project=project) + policy_member3.notify_level = NotifyLevel.none + policy_member3.save() member4 = f.MembershipFactory.create(project=project, role=role1) + policy_member4 = member4.user.notify_policies.get(project=project) + policy_member4.notify_level = NotifyLevel.none + policy_member4.save() member5 = f.MembershipFactory.create(project=project, role=role2) + policy_member5 = member5.user.notify_policies.get(project=project) + policy_member5.notify_level = NotifyLevel.none + policy_member5.save() inactive_member1 = f.MembershipFactory.create(project=project, role=role1) inactive_member1.user.is_active = False inactive_member1.user.save() @@ -120,14 +174,13 @@ def test_users_to_notify(): policy_model_cls = apps.get_model("notifications", "NotifyPolicy") - policy1 = policy_model_cls.objects.get(user=member1.user) - policy2 = policy_model_cls.objects.get(user=member3.user) - policy3 = policy_model_cls.objects.get(user=inactive_member1.user) - policy3.notify_level = NotifyLevel.watch - policy3.save() - policy4 = policy_model_cls.objects.get(user=system_member1.user) - policy4.notify_level = NotifyLevel.watch - policy4.save() + policy_inactive_member1 = policy_model_cls.objects.get(user=inactive_member1.user) + policy_inactive_member1.notify_level = NotifyLevel.all + policy_inactive_member1.save() + + policy_system_member1 = policy_model_cls.objects.get(user=system_member1.user) + policy_system_member1.notify_level = NotifyLevel.all + policy_system_member1.save() history = MagicMock() history.owner = member2.user @@ -136,51 +189,173 @@ def test_users_to_notify(): # Test basic description modifications issue.description = "test1" issue.save() + policy_member4.notify_level = NotifyLevel.all + policy_member4.save() users = services.get_users_to_notify(issue) assert len(users) == 1 assert tuple(users)[0] == issue.get_owner() # Test watch notify level in one member - policy1.notify_level = NotifyLevel.watch - policy1.save() + policy_member1.notify_level = NotifyLevel.all + policy_member1.save() users = services.get_users_to_notify(issue) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} # Test with watchers - issue.watchers.add(member3.user) + issue.add_watcher(member3.user) + policy_member3.notify_level = NotifyLevel.all + policy_member3.save() users = services.get_users_to_notify(issue) assert len(users) == 3 assert users == {member1.user, member3.user, issue.get_owner()} # Test with watchers with ignore policy - policy2.notify_level = NotifyLevel.ignore - policy2.save() + policy_member3.notify_level = NotifyLevel.none + policy_member3.save() - issue.watchers.add(member3.user) + issue.add_watcher(member3.user) users = services.get_users_to_notify(issue) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} # Test with watchers without permissions - issue.watchers.add(member5.user) + issue.add_watcher(member5.user) users = services.get_users_to_notify(issue) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} # Test with inactive user - issue.watchers.add(inactive_member1.user) + issue.add_watcher(inactive_member1.user) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} # Test with system user - issue.watchers.add(system_member1.user) + issue.add_watcher(system_member1.user) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} -def test_send_notifications_using_services_method(settings, mail): +def test_watching_users_to_notify_on_issue_modification_1(): + # If: + # - the user is watching the issue + # - the user is not watching the project + # - the notify policy is watch + # Then: + # - email is sent + project = f.ProjectFactory.create(anon_permissions= ["view_issues"]) + issue = f.IssueFactory.create(project=project) + watching_user = f.UserFactory() + issue.add_watcher(watching_user) + watching_user_policy = services.get_notify_policy(project, watching_user) + issue.description = "test1" + issue.save() + watching_user_policy.notify_level = NotifyLevel.all + users = services.get_users_to_notify(issue) + assert users == {watching_user, issue.owner} + + +def test_watching_users_to_notify_on_issue_modification_2(): + # If: + # - the user is watching the issue + # - the user is not watching the project + # - the notify policy is involved + # Then: + # - email is sent + project = f.ProjectFactory.create(anon_permissions= ["view_issues"]) + issue = f.IssueFactory.create(project=project) + watching_user = f.UserFactory() + issue.add_watcher(watching_user) + watching_user_policy = services.get_notify_policy(project, watching_user) + issue.description = "test1" + issue.save() + watching_user_policy.notify_level = NotifyLevel.involved + users = services.get_users_to_notify(issue) + assert users == {watching_user, issue.owner} + + +def test_watching_users_to_notify_on_issue_modification_3(): + # If: + # - the user is watching the issue + # - the user is not watching the project + # - the notify policy is ignore + # Then: + # - email is not sent + project = f.ProjectFactory.create(anon_permissions= ["view_issues"]) + issue = f.IssueFactory.create(project=project) + watching_user = f.UserFactory() + issue.add_watcher(watching_user) + watching_user_policy = services.get_notify_policy(project, watching_user) + issue.description = "test1" + issue.save() + watching_user_policy.notify_level = NotifyLevel.none + watching_user_policy.save() + users = services.get_users_to_notify(issue) + assert users == {issue.owner} + + +def test_watching_users_to_notify_on_issue_modification_4(): + # If: + # - the user is not watching the issue + # - the user is watching the project + # - the notify policy is ignore + # Then: + # - email is not sent + project = f.ProjectFactory.create(anon_permissions= ["view_issues"]) + issue = f.IssueFactory.create(project=project) + watching_user = f.UserFactory() + project.add_watcher(watching_user) + watching_user_policy = services.get_notify_policy(project, watching_user) + issue.description = "test1" + issue.save() + watching_user_policy.notify_level = NotifyLevel.none + watching_user_policy.save() + users = services.get_users_to_notify(issue) + assert users == {issue.owner} + + +def test_watching_users_to_notify_on_issue_modification_5(): + # If: + # - the user is not watching the issue + # - the user is watching the project + # - the notify policy is watch + # Then: + # - email is sent + project = f.ProjectFactory.create(anon_permissions= ["view_issues"]) + issue = f.IssueFactory.create(project=project) + watching_user = f.UserFactory() + project.add_watcher(watching_user) + watching_user_policy = services.get_notify_policy(project, watching_user) + issue.description = "test1" + issue.save() + watching_user_policy.notify_level = NotifyLevel.all + watching_user_policy.save() + users = services.get_users_to_notify(issue) + assert users == {watching_user, issue.owner} + + +def test_watching_users_to_notify_on_issue_modification_6(): + # If: + # - the user is not watching the issue + # - the user is watching the project + # - the notify policy is involved + # Then: + # - email is sent + project = f.ProjectFactory.create(anon_permissions= ["view_issues"]) + issue = f.IssueFactory.create(project=project) + watching_user = f.UserFactory() + project.add_watcher(watching_user) + watching_user_policy = services.get_notify_policy(project, watching_user) + issue.description = "test1" + issue.save() + watching_user_policy.notify_level = NotifyLevel.involved + watching_user_policy.save() + users = services.get_users_to_notify(issue) + assert users == {watching_user, issue.owner} + + +def test_send_notifications_using_services_method_for_user_stories(settings, mail): settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1 project = f.ProjectFactory.create() @@ -188,38 +363,34 @@ def test_send_notifications_using_services_method(settings, mail): member1 = f.MembershipFactory.create(project=project, role=role) member2 = f.MembershipFactory.create(project=project, role=role) - history_change = MagicMock() - history_change.user = {"pk": member1.user.pk} - history_change.comment = "" - history_change.type = HistoryType.change - history_change.is_hidden = False - - history_create = MagicMock() - history_create.user = {"pk": member1.user.pk} - history_create.comment = "" - history_create.type = HistoryType.create - history_create.is_hidden = False - - history_delete = MagicMock() - history_delete.user = {"pk": member1.user.pk} - history_delete.comment = "" - history_delete.type = HistoryType.delete - history_delete.is_hidden = False - - # Issues - issue = f.IssueFactory.create(project=project, owner=member2.user) - take_snapshot(issue, user=issue.owner) - services.send_notifications(issue, - history=history_create) - - services.send_notifications(issue, - history=history_change) - - services.send_notifications(issue, - history=history_delete) - - # Userstories us = f.UserStoryFactory.create(project=project, owner=member2.user) + history_change = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.change, + key="userstories.userstory:{}".format(us.id), + is_hidden=False, + diff=[] + ) + + history_create = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.create, + key="userstories.userstory:{}".format(us.id), + is_hidden=False, + diff=[] + ) + + history_delete = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="test:delete", + type=HistoryType.delete, + key="userstories.userstory:{}".format(us.id), + is_hidden=False, + diff=[] + ) + take_snapshot(us, user=us.owner) services.send_notifications(us, history=history_create) @@ -230,8 +401,90 @@ def test_send_notifications_using_services_method(settings, mail): services.send_notifications(us, history=history_delete) - # Tasks + assert models.HistoryChangeNotification.objects.count() == 3 + assert len(mail.outbox) == 0 + time.sleep(1) + services.process_sync_notifications() + assert len(mail.outbox) == 3 + + # test headers + domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"] + for msg in mail.outbox: + m_id = "{project_slug}/{msg_id}".format( + project_slug=project.slug, + msg_id=us.ref + ) + + message_id = "<{m_id}/".format(m_id=m_id) + message_id_domain = "@{domain}>".format(domain=domain) + in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain) + list_id = "Taiga/{p_name} " \ + .format(p_name=project.name, p_slug=project.slug, domain=domain) + + assert msg.extra_headers + headers = msg.extra_headers + + # can't test the time part because it's set when sending + # check what we can + assert 'Message-ID' in headers + assert message_id in headers.get('Message-ID') + assert message_id_domain in headers.get('Message-ID') + + assert 'In-Reply-To' in headers + assert in_reply_to == headers.get('In-Reply-To') + assert 'References' in headers + assert in_reply_to == headers.get('References') + + assert 'List-ID' in headers + assert list_id == headers.get('List-ID') + + assert 'Thread-Index' in headers + # always is b64 encoded 22 bytes + assert len(base64.b64decode(headers.get('Thread-Index'))) == 22 + + # hashes should match for identical ids and times + # we check the actual method in test_ms_thread_id() + msg_time = headers.get('Message-ID').split('/')[2].split('@')[0] + msg_ts = datetime.datetime.fromtimestamp(int(msg_time)) + assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index') + + +def test_send_notifications_using_services_method_for_tasks(settings, mail): + settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1 + + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_issues', 'view_us', 'view_tasks', 'view_wiki_pages']) + member1 = f.MembershipFactory.create(project=project, role=role) + member2 = f.MembershipFactory.create(project=project, role=role) + task = f.TaskFactory.create(project=project, owner=member2.user) + history_change = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.change, + key="tasks.task:{}".format(task.id), + is_hidden=False, + diff=[] + ) + + history_create = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.create, + key="tasks.task:{}".format(task.id), + is_hidden=False, + diff=[] + ) + + history_delete = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="test:delete", + type=HistoryType.delete, + key="tasks.task:{}".format(task.id), + is_hidden=False, + diff=[] + ) + take_snapshot(task, user=task.owner) services.send_notifications(task, history=history_create) @@ -242,8 +495,183 @@ def test_send_notifications_using_services_method(settings, mail): services.send_notifications(task, history=history_delete) - # Wiki pages + assert models.HistoryChangeNotification.objects.count() == 3 + assert len(mail.outbox) == 0 + time.sleep(1) + services.process_sync_notifications() + assert len(mail.outbox) == 3 + + # test headers + domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"] + for msg in mail.outbox: + m_id = "{project_slug}/{msg_id}".format( + project_slug=project.slug, + msg_id=task.ref + ) + + message_id = "<{m_id}/".format(m_id=m_id) + message_id_domain = "@{domain}>".format(domain=domain) + in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain) + list_id = "Taiga/{p_name} " \ + .format(p_name=project.name, p_slug=project.slug, domain=domain) + + assert msg.extra_headers + headers = msg.extra_headers + + # can't test the time part because it's set when sending + # check what we can + assert 'Message-ID' in headers + assert message_id in headers.get('Message-ID') + assert message_id_domain in headers.get('Message-ID') + + assert 'In-Reply-To' in headers + assert in_reply_to == headers.get('In-Reply-To') + assert 'References' in headers + assert in_reply_to == headers.get('References') + + assert 'List-ID' in headers + assert list_id == headers.get('List-ID') + + assert 'Thread-Index' in headers + # always is b64 encoded 22 bytes + assert len(base64.b64decode(headers.get('Thread-Index'))) == 22 + + # hashes should match for identical ids and times + # we check the actual method in test_ms_thread_id() + msg_time = headers.get('Message-ID').split('/')[2].split('@')[0] + msg_ts = datetime.datetime.fromtimestamp(int(msg_time)) + assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index') + + +def test_send_notifications_using_services_method_for_issues(settings, mail): + settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1 + + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_issues', 'view_us', 'view_tasks', 'view_wiki_pages']) + member1 = f.MembershipFactory.create(project=project, role=role) + member2 = f.MembershipFactory.create(project=project, role=role) + + issue = f.IssueFactory.create(project=project, owner=member2.user) + history_change = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.change, + key="issues.issue:{}".format(issue.id), + is_hidden=False, + diff=[] + ) + + history_create = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.create, + key="issues.issue:{}".format(issue.id), + is_hidden=False, + diff=[] + ) + + history_delete = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="test:delete", + type=HistoryType.delete, + key="issues.issue:{}".format(issue.id), + is_hidden=False, + diff=[] + ) + + take_snapshot(issue, user=issue.owner) + services.send_notifications(issue, + history=history_create) + + services.send_notifications(issue, + history=history_change) + + services.send_notifications(issue, + history=history_delete) + + assert models.HistoryChangeNotification.objects.count() == 3 + assert len(mail.outbox) == 0 + time.sleep(1) + services.process_sync_notifications() + assert len(mail.outbox) == 3 + + # test headers + domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"] + for msg in mail.outbox: + m_id = "{project_slug}/{msg_id}".format( + project_slug=project.slug, + msg_id=issue.ref + ) + + message_id = "<{m_id}/".format(m_id=m_id) + message_id_domain = "@{domain}>".format(domain=domain) + in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain) + list_id = "Taiga/{p_name} " \ + .format(p_name=project.name, p_slug=project.slug, domain=domain) + + assert msg.extra_headers + headers = msg.extra_headers + + # can't test the time part because it's set when sending + # check what we can + assert 'Message-ID' in headers + assert message_id in headers.get('Message-ID') + assert message_id_domain in headers.get('Message-ID') + + assert 'In-Reply-To' in headers + assert in_reply_to == headers.get('In-Reply-To') + assert 'References' in headers + assert in_reply_to == headers.get('References') + + assert 'List-ID' in headers + assert list_id == headers.get('List-ID') + + assert 'Thread-Index' in headers + # always is b64 encoded 22 bytes + assert len(base64.b64decode(headers.get('Thread-Index'))) == 22 + + # hashes should match for identical ids and times + # we check the actual method in test_ms_thread_id() + msg_time = headers.get('Message-ID').split('/')[2].split('@')[0] + msg_ts = datetime.datetime.fromtimestamp(int(msg_time)) + assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index') + + +def test_send_notifications_using_services_method_for_wiki_pages(settings, mail): + settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL = 1 + + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_issues', 'view_us', 'view_tasks', 'view_wiki_pages']) + member1 = f.MembershipFactory.create(project=project, role=role) + member2 = f.MembershipFactory.create(project=project, role=role) + wiki = f.WikiPageFactory.create(project=project, owner=member2.user) + history_change = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.change, + key="wiki.wikipage:{}".format(wiki.id), + is_hidden=False, + diff=[] + ) + + history_create = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="", + type=HistoryType.create, + key="wiki.wikipage:{}".format(wiki.id), + is_hidden=False, + diff=[] + ) + + history_delete = f.HistoryEntryFactory.create( + user={"pk": member1.user.id}, + comment="test:delete", + type=HistoryType.delete, + key="wiki.wikipage:{}".format(wiki.id), + is_hidden=False, + diff=[] + ) take_snapshot(wiki, user=wiki.owner) services.send_notifications(wiki, history=history_create) @@ -254,11 +682,52 @@ def test_send_notifications_using_services_method(settings, mail): services.send_notifications(wiki, history=history_delete) - assert models.HistoryChangeNotification.objects.count() == 12 + assert models.HistoryChangeNotification.objects.count() == 3 assert len(mail.outbox) == 0 time.sleep(1) services.process_sync_notifications() - assert len(mail.outbox) == 12 + assert len(mail.outbox) == 3 + + # test headers + domain = settings.SITES["api"]["domain"].split(":")[0] or settings.SITES["api"]["domain"] + for msg in mail.outbox: + m_id = "{project_slug}/{msg_id}".format( + project_slug=project.slug, + msg_id=wiki.slug + ) + + message_id = "<{m_id}/".format(m_id=m_id) + message_id_domain = "@{domain}>".format(domain=domain) + in_reply_to = "<{m_id}@{domain}>".format(m_id=m_id, domain=domain) + list_id = "Taiga/{p_name} " \ + .format(p_name=project.name, p_slug=project.slug, domain=domain) + + assert msg.extra_headers + headers = msg.extra_headers + + # can't test the time part because it's set when sending + # check what we can + assert 'Message-ID' in headers + assert message_id in headers.get('Message-ID') + assert message_id_domain in headers.get('Message-ID') + + assert 'In-Reply-To' in headers + assert in_reply_to == headers.get('In-Reply-To') + assert 'References' in headers + assert in_reply_to == headers.get('References') + + assert 'List-ID' in headers + assert list_id == headers.get('List-ID') + + assert 'Thread-Index' in headers + # always is b64 encoded 22 bytes + assert len(base64.b64decode(headers.get('Thread-Index'))) == 22 + + # hashes should match for identical ids and times + # we check the actual method in test_ms_thread_id() + msg_time = headers.get('Message-ID').split('/')[2].split('@')[0] + msg_ts = datetime.datetime.fromtimestamp(int(msg_time)) + assert services.make_ms_thread_index(in_reply_to, msg_ts) == headers.get('Thread-Index') def test_resource_notification_test(client, settings, mail): @@ -313,11 +782,11 @@ def test_watchers_assignation_for_issue(client): issue = f.create_issue(project=project1, owner=user1) data = {"version": issue.version, - "watchers": [user1.pk]} + "watchersa": [user1.pk]} url = reverse("issues-detail", args=[issue.pk]) response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 200, response.content + assert response.status_code == 200, str(response.content) issue = f.create_issue(project=project1, owner=user1) data = {"version": issue.version, @@ -356,22 +825,22 @@ def test_watchers_assignation_for_task(client): user2 = f.UserFactory.create() project1 = f.ProjectFactory.create(owner=user1) project2 = f.ProjectFactory.create(owner=user2) - role1 = f.RoleFactory.create(project=project1) + 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=project2, user=user2, role=role2) client.login(user1) - task = f.create_task(project=project1, owner=user1) + task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1, user_story=None) data = {"version": task.version, "watchers": [user1.pk]} url = reverse("tasks-detail", args=[task.pk]) response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 200, response.content + assert response.status_code == 200, str(response.content) - task = f.create_task(project=project1, owner=user1) + task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) data = {"version": task.version, "watchers": [user1.pk, user2.pk]} @@ -379,7 +848,7 @@ def test_watchers_assignation_for_task(client): response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 - task = f.create_task(project=project1, owner=user1) + task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) data = dict(TaskSerializer(task).data) data["id"] = None data["version"] = None @@ -391,7 +860,7 @@ def test_watchers_assignation_for_task(client): # Test the impossible case when project is not # exists in create request, and validator works as expected - task = f.create_task(project=project1, owner=user1) + task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) data = dict(TaskSerializer(task).data) data["id"] = None @@ -415,15 +884,15 @@ def test_watchers_assignation_for_us(client): client.login(user1) - us = f.create_userstory(project=project1, owner=user1) + us = f.create_userstory(project=project1, owner=user1, status__project=project1) data = {"version": us.version, "watchers": [user1.pk]} url = reverse("userstories-detail", args=[us.pk]) response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 200 + assert response.status_code == 200, str(response.content) - us = f.create_userstory(project=project1, owner=user1) + us = f.create_userstory(project=project1, owner=user1, status__project=project1) data = {"version": us.version, "watchers": [user1.pk, user2.pk]} @@ -431,7 +900,7 @@ def test_watchers_assignation_for_us(client): response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 - us = f.create_userstory(project=project1, owner=user1) + us = f.create_userstory(project=project1, owner=user1, status__project=project1) data = dict(UserStorySerializer(us).data) data["id"] = None data["version"] = None @@ -443,7 +912,7 @@ def test_watchers_assignation_for_us(client): # Test the impossible case when project is not # exists in create request, and validator works as expected - us = f.create_userstory(project=project1, owner=user1) + us = f.create_userstory(project=project1, owner=user1, status__project=project1) data = dict(UserStorySerializer(us).data) data["id"] = None @@ -463,4 +932,38 @@ def test_retrieve_notify_policies_by_anonymous_user(client): url = reverse("notifications-detail", args=[policy.pk]) response = client.get(url, content_type="application/json") assert response.status_code == 404, response.status_code - assert json.loads(response.content.decode("utf-8"))["_error_message"] == "No NotifyPolicy matches the given query.", response.content + assert response.data["_error_message"] == "No NotifyPolicy matches the given query.", str(response.content) + + +def test_ms_thread_id(): + id = '' + now = datetime.datetime.now() + + index = services.make_ms_thread_index(id, now) + parsed = parse_ms_thread_index(index) + + assert parsed[0] == hashlib.md5(id.encode('utf-8')).hexdigest() + # always only one time + assert (now - parsed[1][0]).seconds <= 2 + + +# see http://stackoverflow.com/questions/27374077/parsing-thread-index-mail-header-with-python +def parse_ms_thread_index(index): + s = base64.b64decode(index) + + # ours are always md5 digests + guid = binascii.hexlify(s[6:22]).decode('utf-8') + + # if we had real guids, we'd do something like + # guid = struct.unpack('>IHHQ', s[6:22]) + # guid = '%08X-%04X-%04X-%04X-%12X' % (guid[0], guid[1], guid[2], (guid[3] >> 48) & 0xFFFF, guid[3] & 0xFFFFFFFFFFFF) + + f = struct.unpack('>Q', s[:6] + b'\0\0')[0] + ts = [datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=f//10)] + + # for the 5 byte appendixes that we won't use + for n in range(22, len(s), 5): + f = struct.unpack('>I', s[n:n+4])[0] + ts.append(ts[-1] + datetime.timedelta(microseconds=(f << 18)//10)) + + return guid, ts diff --git a/tests/integration/test_occ.py b/tests/integration/test_occ.py index 9826cf0e..2cbdaced 100644 --- a/tests/integration/test_occ.py +++ b/tests/integration/test_occ.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -61,7 +61,7 @@ def test_invalid_concurrent_save_for_issue(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.content - issue_id = json.loads(response.content)["id"] + issue_id = response.data["id"] url = reverse("issues-detail", args=(issue_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -90,7 +90,7 @@ def test_valid_concurrent_save_for_issue_different_versions(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.content - issue_id = json.loads(response.content)["id"] + issue_id = response.data["id"] url = reverse("issues-detail", args=(issue_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -119,7 +119,7 @@ def test_valid_concurrent_save_for_issue_different_fields(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.content - issue_id = json.loads(response.content)["id"] + issue_id = response.data["id"] url = reverse("issues-detail", args=(issue_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -143,7 +143,7 @@ def test_invalid_concurrent_save_for_wiki_page(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.content - wiki_id = json.loads(response.content)["id"] + wiki_id = response.data["id"] url = reverse("wiki-detail", args=(wiki_id,)) data = {"version": 1, "content": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -167,7 +167,7 @@ def test_valid_concurrent_save_for_wiki_page_different_versions(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.content - wiki_id = json.loads(response.content)["id"] + wiki_id = response.data["id"] url = reverse("wiki-detail", args=(wiki_id,)) data = {"version": 1, "content": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -194,7 +194,7 @@ def test_invalid_concurrent_save_for_us(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - userstory_id = json.loads(response.content)["id"] + userstory_id = response.data["id"] url = reverse("userstories-detail", args=(userstory_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -220,7 +220,7 @@ def test_valid_concurrent_save_for_us_different_versions(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - userstory_id = json.loads(response.content)["id"] + userstory_id = response.data["id"] url = reverse("userstories-detail", args=(userstory_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -246,7 +246,7 @@ def test_valid_concurrent_save_for_us_different_fields(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - userstory_id = json.loads(response.content)["id"] + userstory_id = response.data["id"] url = reverse("userstories-detail", args=(userstory_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -272,7 +272,7 @@ def test_invalid_concurrent_save_for_task(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - task_id = json.loads(response.content)["id"] + task_id = response.data["id"] url = reverse("tasks-detail", args=(task_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -298,7 +298,7 @@ def test_valid_concurrent_save_for_task_different_versions(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - task_id = json.loads(response.content)["id"] + task_id = response.data["id"] url = reverse("tasks-detail", args=(task_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -324,7 +324,7 @@ def test_valid_concurrent_save_for_task_different_fields(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - task_id = json.loads(response.content)["id"] + task_id = response.data["id"] url = reverse("tasks-detail", args=(task_id,)) data = {"version": 1, "subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") @@ -351,7 +351,7 @@ def test_invalid_save_without_version_parameter(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - task_id = json.loads(response.content)["id"] + task_id = response.data["id"] url = reverse("tasks-detail", args=(task_id,)) data = {"subject": "test 1"} response = client.patch(url, json.dumps(data), content_type="application/json") diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index fe3e53be..2a4f8e5c 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -45,6 +45,46 @@ def test_partially_update_project(client): assert response.status_code == 400 +def test_us_status_is_closed_changed_recalc_us_is_closed(client): + us_status = f.UserStoryStatusFactory(is_closed=False) + user_story = f.UserStoryFactory.create(project=us_status.project, status=us_status) + + assert user_story.is_closed is False + + us_status.is_closed = True + us_status.save() + + user_story = user_story.__class__.objects.get(pk=user_story.pk) + assert user_story.is_closed is True + + us_status.is_closed = False + us_status.save() + + user_story = user_story.__class__.objects.get(pk=user_story.pk) + assert user_story.is_closed is False + + +def test_task_status_is_closed_changed_recalc_us_is_closed(client): + us_status = f.UserStoryStatusFactory() + user_story = f.UserStoryFactory.create(project=us_status.project, status=us_status) + task_status = f.TaskStatusFactory.create(project=us_status.project, is_closed=False) + task = f.TaskFactory.create(project=us_status.project, status=task_status, user_story=user_story) + + assert user_story.is_closed is False + + task_status.is_closed = True + task_status.save() + + user_story = user_story.__class__.objects.get(pk=user_story.pk) + assert user_story.is_closed is True + + task_status.is_closed = False + task_status.save() + + user_story = user_story.__class__.objects.get(pk=user_story.pk) + assert user_story.is_closed is False + + 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) @@ -200,7 +240,7 @@ def test_leave_project_valid_membership_only_owner(client): url = reverse("projects-leave", args=(project.id,)) response = client.post(url) assert response.status_code == 403 - assert json.loads(response.content)["_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 there are no more owners" def test_leave_project_invalid_membership(client): @@ -225,7 +265,7 @@ def test_leave_project_respect_watching_items(client): url = reverse("projects-leave", args=(project.id,)) response = client.post(url) assert response.status_code == 200 - assert list(issue.watchers.all()) == [user] + assert issue.watchers == [user] def test_delete_membership_only_owner(client): @@ -237,7 +277,7 @@ def test_delete_membership_only_owner(client): url = reverse("memberships-detail", args=(membership.id,)) response = client.delete(url) assert response.status_code == 400 - assert json.loads(response.content)["_error_message"] == "At least one of the user must be an active admin" + assert response.data["_error_message"] == "At least one of the user must be an active admin" def test_edit_membership_only_owner(client): @@ -355,7 +395,7 @@ def test_projects_user_order(client): url = reverse("projects-list") url = "%s?member=%s" % (url, user.id) response = client.json.get(url) - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert response.status_code == 200 assert(response_content[0]["id"] == project_1.id) @@ -363,6 +403,6 @@ def test_projects_user_order(client): url = reverse("projects-list") url = "%s?member=%s&order_by=memberships__user_order" % (url, user.id) response = client.json.get(url) - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert response.status_code == 200 assert(response_content[0]["id"] == project_2.id) diff --git a/tests/integration/test_references_sequences.py b/tests/integration/test_references_sequences.py index 10653cea..815bf420 100644 --- a/tests/integration/test_references_sequences.py +++ b/tests/integration/test_references_sequences.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -17,6 +17,8 @@ import pytest +from django.core.urlresolvers import reverse + from .. import factories @@ -74,3 +76,106 @@ def test_unique_reference_per_project(seq, refmodels): project.delete() assert not seq.exists(seqname) + + +@pytest.mark.django_db +def test_regenerate_us_reference_on_project_change(seq, refmodels): + project1 = factories.ProjectFactory.create() + seqname1 = refmodels.make_sequence_name(project1) + project2 = factories.ProjectFactory.create() + seqname2 = refmodels.make_sequence_name(project2) + + seq.alter(seqname1, 100) + seq.alter(seqname2, 200) + + user_story = factories.UserStoryFactory.create(project=project1) + assert user_story.ref == 101 + + user_story.subject = "other" + user_story.save() + assert user_story.ref == 101 + + user_story.project = project2 + user_story.save() + + assert user_story.ref == 201 + +@pytest.mark.django_db +def test_regenerate_task_reference_on_project_change(seq, refmodels): + project1 = factories.ProjectFactory.create() + seqname1 = refmodels.make_sequence_name(project1) + project2 = factories.ProjectFactory.create() + seqname2 = refmodels.make_sequence_name(project2) + + seq.alter(seqname1, 100) + seq.alter(seqname2, 200) + + task = factories.TaskFactory.create(project=project1) + assert task.ref == 101 + + task.subject = "other" + task.save() + assert task.ref == 101 + + task.project = project2 + task.save() + + assert task.ref == 201 + +@pytest.mark.django_db +def test_regenerate_issue_reference_on_project_change(seq, refmodels): + project1 = factories.ProjectFactory.create() + seqname1 = refmodels.make_sequence_name(project1) + project2 = factories.ProjectFactory.create() + seqname2 = refmodels.make_sequence_name(project2) + + seq.alter(seqname1, 100) + seq.alter(seqname2, 200) + + issue = factories.IssueFactory.create(project=project1) + assert issue.ref == 101 + + issue.subject = "other" + issue.save() + assert issue.ref == 101 + + issue.project = project2 + issue.save() + + assert issue.ref == 201 + + +@pytest.mark.django_db +def test_params_validation_in_api_request(client, refmodels): + user = factories.UserFactory.create() + 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) + + milestone = factories.MilestoneFactory.create(project=project) + us = factories.UserStoryFactory.create(project=project) + task = factories.TaskFactory.create(project=project) + issue = factories.IssueFactory.create(project=project) + wiki_page = factories.WikiPageFactory.create(project=project) + + client.login(user) + + url = reverse("resolver-list") + response = client.json.get(url) + assert response.status_code == 400 + response = client.json.get("{}?project={}".format(url, project.slug)) + assert response.status_code == 200 + response = client.json.get("{}?project={}&ref={}".format(url, project.slug, us.ref)) + assert response.status_code == 200 + response = client.json.get("{}?project={}&ref={}&us={}".format(url, project.slug, us.ref, us.ref)) + assert response.status_code == 400 + response = client.json.get("{}?project={}&ref={}&task={}".format(url, project.slug, us.ref, task.ref)) + assert response.status_code == 400 + response = client.json.get("{}?project={}&ref={}&issue={}".format(url, project.slug, us.ref, issue.ref)) + assert response.status_code == 400 + response = client.json.get("{}?project={}&us={}&task={}".format(url, project.slug, us.ref, task.ref)) + assert response.status_code == 200 + response = client.json.get("{}?project={}&ref={}&milestone={}".format(url, project.slug, us.ref, + milestone.slug)) + assert response.status_code == 200 diff --git a/tests/integration/test_roles.py b/tests/integration/test_roles.py index 70665aff..0bc773e7 100644 --- a/tests/integration/test_roles.py +++ b/tests/integration/test_roles.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index 606f3304..c191682f 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -25,7 +25,7 @@ from taiga.permissions.permissions import MEMBERS_PERMISSIONS from tests.utils import disconnect_signals, reconnect_signals -pytestmark = pytest.mark.django_db +pytestmark = pytest.mark.django_db(transaction=True) def setup_module(module): @@ -124,10 +124,11 @@ def test_search_text_query_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "back"}) assert response.status_code == 200 - assert response.data["count"] == 2 + assert response.data["count"] == 3 assert len(response.data["userstories"]) == 1 assert len(response.data["tasks"]) == 1 - assert len(response.data["issues"]) == 0 + # Back is a backend substring + assert len(response.data["issues"]) == 1 assert len(response.data["wikipages"]) == 0 diff --git a/tests/integration/test_stars.py b/tests/integration/test_stars.py deleted file mode 100644 index eddb03a5..00000000 --- a/tests/integration/test_stars.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import pytest -from django.core.urlresolvers import reverse - -from .. import factories as f - -pytestmark = pytest.mark.django_db - - -def test_project_owner_star_project(client): - user = f.UserFactory.create() - project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, is_owner=True, user=user) - url = reverse("projects-star", args=(project.id,)) - - client.login(user) - response = client.post(url) - - assert response.status_code == 200 - - -def test_project_owner_unstar_project(client): - user = f.UserFactory.create() - project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, is_owner=True, user=user) - url = reverse("projects-unstar", args=(project.id,)) - - client.login(user) - response = client.post(url) - - assert response.status_code == 200 - - -def test_project_member_star_project(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) - url = reverse("projects-star", args=(project.id,)) - - client.login(user) - response = client.post(url) - - assert response.status_code == 200 - - -def test_project_member_unstar_project(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) - url = reverse("projects-unstar", args=(project.id,)) - - client.login(user) - response = client.post(url) - - assert response.status_code == 200 - - -def test_list_project_fans(client): - user = f.UserFactory.create() - project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) - fan = f.VoteFactory.create(content_object=project) - url = reverse("projects-fans", args=(project.id,)) - - client.login(user) - response = client.get(url) - - assert response.status_code == 200 - assert response.data[0]['id'] == fan.user.id - - -def test_list_user_starred_projects(client): - user = f.UserFactory.create() - project = f.ProjectFactory() - url = reverse("users-starred", args=(user.id,)) - f.VoteFactory.create(user=user, content_object=project) - - client.login(user) - response = client.get(url) - - assert response.status_code == 200 - assert response.data[0]['id'] == project.id - - -def test_get_project_stars(client): - user = f.UserFactory.create() - project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user, is_owner=True) - url = reverse("projects-detail", args=(project.id,)) - f.VotesFactory.create(content_object=project, count=5) - f.VotesFactory.create(count=3) - - client.login(user) - response = client.get(url) - - assert response.status_code == 200 - assert response.data['stars'] == 5 diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py index 97bce3ff..374985c3 100644 --- a/tests/integration/test_stats.py +++ b/tests/integration/test_stats.py @@ -3,6 +3,9 @@ import pytest from .. import factories as f from tests.utils import disconnect_signals, reconnect_signals +from taiga.projects.services.stats import get_stats_for_project + + pytestmark = pytest.mark.django_db @@ -30,6 +33,8 @@ def data(): m.points2 = f.PointsFactory(project=m.project, value=2) m.points3 = f.PointsFactory(project=m.project, value=4) m.points4 = f.PointsFactory(project=m.project, value=8) + m.points5 = f.PointsFactory(project=m.project, value=16) + m.points6 = f.PointsFactory(project=m.project, value=32) m.open_status = f.UserStoryStatusFactory(is_closed=False) m.closed_status = f.UserStoryStatusFactory(is_closed=True) @@ -37,24 +42,40 @@ def data(): m.role_points1 = f.RolePointsFactory(role=m.role1, points=m.points1, user_story__project=m.project, - user_story__status=m.open_status) + user_story__status=m.open_status, + user_story__milestone=None) m.role_points2 = f.RolePointsFactory(role=m.role1, points=m.points2, user_story__project=m.project, - user_story__status=m.open_status) + user_story__status=m.open_status, + user_story__milestone=None) m.role_points3 = f.RolePointsFactory(role=m.role1, points=m.points3, user_story__project=m.project, - user_story__status=m.open_status) + user_story__status=m.open_status, + user_story__milestone=None) m.role_points4 = f.RolePointsFactory(role=m.project.roles.all()[0], points=m.points4, user_story__project=m.project, + user_story__status=m.open_status, + user_story__milestone=None) + # 5 and 6 are in the same milestone + m.role_points5 = f.RolePointsFactory(role=m.project.roles.all()[0], + points=m.points5, + user_story__project=m.project, user_story__status=m.open_status) + m.role_points6 = f.RolePointsFactory(role=m.project.roles.all()[0], + points=m.points6, + user_story__project=m.project, + user_story__status=m.open_status, + user_story__milestone=m.role_points5.user_story.milestone) m.user_story1 = m.role_points1.user_story m.user_story2 = m.role_points2.user_story m.user_story3 = m.role_points3.user_story m.user_story4 = m.role_points4.user_story + m.user_story5 = m.role_points5.user_story + m.user_story6 = m.role_points6.user_story m.milestone = f.MilestoneFactory(project=m.project) @@ -62,10 +83,10 @@ def data(): def test_project_defined_points(client, data): - assert data.project.defined_points == {data.role1.pk: 15} + assert data.project.defined_points == {data.role1.pk: 63} data.role_points1.role = data.role2 data.role_points1.save() - assert data.project.defined_points == {data.role1.pk: 14, data.role2.pk: 1} + assert data.project.defined_points == {data.role1.pk: 62, data.role2.pk: 1} def test_project_closed_points(client, data): @@ -85,22 +106,29 @@ def test_project_closed_points(client, data): data.user_story4.is_closed = True data.user_story4.save() assert data.project.closed_points == {data.role1.pk: 14, data.role2.pk: 1} + #User story5 milestone isn't closed + data.user_story5.is_closed = True + data.user_story5.save() + assert data.project.closed_points == {data.role1.pk: 30, data.role2.pk: 1} + + project_stats = get_stats_for_project(data.project) + assert project_stats["closed_points"] == 15 def test_project_assigned_points(client, data): - assert data.project.assigned_points == {} + assert data.project.assigned_points == {data.role1.pk: 48} data.role_points1.role = data.role2 data.role_points1.save() - assert data.project.assigned_points == {} + assert data.project.assigned_points == {data.role1.pk: 48} data.user_story1.milestone = data.milestone data.user_story1.save() - assert data.project.assigned_points == {data.role2.pk: 1} + assert data.project.assigned_points == {data.role1.pk: 48, data.role2.pk: 1} data.user_story2.milestone = data.milestone data.user_story2.save() - assert data.project.assigned_points == {data.role1.pk: 2, data.role2.pk: 1} + assert data.project.assigned_points == {data.role1.pk: 50, data.role2.pk: 1} data.user_story3.milestone = data.milestone data.user_story3.save() - assert data.project.assigned_points == {data.role1.pk: 6, data.role2.pk: 1} + assert data.project.assigned_points == {data.role1.pk: 54, data.role2.pk: 1} data.user_story4.milestone = data.milestone data.user_story4.save() - assert data.project.assigned_points == {data.role1.pk: 14, data.role2.pk: 1} + assert data.project.assigned_points == {data.role1.pk: 62, data.role2.pk: 1} diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 61d1954a..08825955 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -54,8 +54,9 @@ def test_create_task_without_status(client): def test_api_update_task_tags(client): - task = f.create_task() - f.MembershipFactory.create(project=task.project, user=task.owner, is_owner=True) + 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) url = reverse("tasks-detail", kwargs={"pk": task.pk}) data = {"tags": ["back", "front"], "version": task.version} @@ -162,6 +163,6 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[17] == attr.name + assert row[19] == attr.name row = next(reader) - assert row[17] == "val1" + assert row[19] == "val1" diff --git a/tests/integration/test_throwttling.py b/tests/integration/test_throwttling.py index ce36d0c4..93c3a26d 100644 --- a/tests/integration/test_throwttling.py +++ b/tests/integration/test_throwttling.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 957c5c91..c96760e9 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -196,8 +196,10 @@ def test_create_membership_timeline(): def test_update_project_timeline(): + user_watcher= factories.UserFactory() project = factories.ProjectFactory.create(name="test project timeline") history_services.take_snapshot(project, user=project.owner) + project.add_watcher(user_watcher) project.name = "test project timeline updated" project.save() history_services.take_snapshot(project, user=project.owner) @@ -206,11 +208,18 @@ def test_update_project_timeline(): assert project_timeline[0].data["project"]["name"] == "test project timeline updated" assert project_timeline[0].data["values_diff"]["name"][0] == "test project timeline" assert project_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "projects.project.change" + assert user_watcher_timeline[0].data["project"]["name"] == "test project timeline updated" + assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test project timeline" + assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test project timeline updated" def test_update_milestone_timeline(): + user_watcher= factories.UserFactory() milestone = factories.MilestoneFactory.create(name="test milestone timeline") history_services.take_snapshot(milestone, user=milestone.owner) + milestone.add_watcher(user_watcher) milestone.name = "test milestone timeline updated" milestone.save() history_services.take_snapshot(milestone, user=milestone.owner) @@ -219,11 +228,18 @@ def test_update_milestone_timeline(): assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline updated" assert project_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline" assert project_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "milestones.milestone.change" + assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline updated" + assert user_watcher_timeline[0].data["values_diff"]["name"][0] == "test milestone timeline" + assert user_watcher_timeline[0].data["values_diff"]["name"][1] == "test milestone timeline updated" def test_update_user_story_timeline(): + user_watcher= factories.UserFactory() user_story = factories.UserStoryFactory.create(subject="test us timeline") history_services.take_snapshot(user_story, user=user_story.owner) + user_story.add_watcher(user_watcher) user_story.subject = "test us timeline updated" user_story.save() history_services.take_snapshot(user_story, user=user_story.owner) @@ -232,11 +248,18 @@ def test_update_user_story_timeline(): assert project_timeline[0].data["userstory"]["subject"] == "test us timeline updated" assert project_timeline[0].data["values_diff"]["subject"][0] == "test us timeline" assert project_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "userstories.userstory.change" + assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline updated" + assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test us timeline" + assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test us timeline updated" def test_update_issue_timeline(): + user_watcher= factories.UserFactory() issue = factories.IssueFactory.create(subject="test issue timeline") history_services.take_snapshot(issue, user=issue.owner) + issue.add_watcher(user_watcher) issue.subject = "test issue timeline updated" issue.save() history_services.take_snapshot(issue, user=issue.owner) @@ -245,11 +268,18 @@ def test_update_issue_timeline(): assert project_timeline[0].data["issue"]["subject"] == "test issue timeline updated" assert project_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline" assert project_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "issues.issue.change" + assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline updated" + assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test issue timeline" + assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test issue timeline updated" def test_update_task_timeline(): + user_watcher= factories.UserFactory() task = factories.TaskFactory.create(subject="test task timeline") history_services.take_snapshot(task, user=task.owner) + task.add_watcher(user_watcher) task.subject = "test task timeline updated" task.save() history_services.take_snapshot(task, user=task.owner) @@ -258,11 +288,18 @@ def test_update_task_timeline(): assert project_timeline[0].data["task"]["subject"] == "test task timeline updated" assert project_timeline[0].data["values_diff"]["subject"][0] == "test task timeline" assert project_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "tasks.task.change" + assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline updated" + assert user_watcher_timeline[0].data["values_diff"]["subject"][0] == "test task timeline" + assert user_watcher_timeline[0].data["values_diff"]["subject"][1] == "test task timeline updated" def test_update_wiki_page_timeline(): + user_watcher= factories.UserFactory() page = factories.WikiPageFactory.create(slug="test wiki page timeline") history_services.take_snapshot(page, user=page.owner) + page.add_watcher(user_watcher) page.slug = "test wiki page timeline updated" page.save() history_services.take_snapshot(page, user=page.owner) @@ -271,6 +308,11 @@ def test_update_wiki_page_timeline(): assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated" assert project_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline" assert project_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "wiki.wikipage.change" + assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline updated" + assert user_watcher_timeline[0].data["values_diff"]["slug"][0] == "test wiki page timeline" + assert user_watcher_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated" def test_update_membership_timeline(): @@ -298,50 +340,80 @@ def test_update_membership_timeline(): def test_delete_project_timeline(): project = factories.ProjectFactory.create(name="test project timeline") + user_watcher= factories.UserFactory() + project.add_watcher(user_watcher) history_services.take_snapshot(project, user=project.owner, delete=True) user_timeline = service.get_project_timeline(project) assert user_timeline[0].event_type == "projects.project.delete" assert user_timeline[0].data["project"]["id"] == project.id + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "projects.project.delete" + assert user_watcher_timeline[0].data["project"]["id"] == project.id def test_delete_milestone_timeline(): milestone = factories.MilestoneFactory.create(name="test milestone timeline") + user_watcher= factories.UserFactory() + milestone.add_watcher(user_watcher) history_services.take_snapshot(milestone, user=milestone.owner, delete=True) project_timeline = service.get_project_timeline(milestone.project) assert project_timeline[0].event_type == "milestones.milestone.delete" assert project_timeline[0].data["milestone"]["name"] == "test milestone timeline" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "milestones.milestone.delete" + assert user_watcher_timeline[0].data["milestone"]["name"] == "test milestone timeline" def test_delete_user_story_timeline(): user_story = factories.UserStoryFactory.create(subject="test us timeline") + user_watcher= factories.UserFactory() + user_story.add_watcher(user_watcher) history_services.take_snapshot(user_story, user=user_story.owner, delete=True) project_timeline = service.get_project_timeline(user_story.project) assert project_timeline[0].event_type == "userstories.userstory.delete" assert project_timeline[0].data["userstory"]["subject"] == "test us timeline" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "userstories.userstory.delete" + assert user_watcher_timeline[0].data["userstory"]["subject"] == "test us timeline" def test_delete_issue_timeline(): issue = factories.IssueFactory.create(subject="test issue timeline") + user_watcher= factories.UserFactory() + issue.add_watcher(user_watcher) history_services.take_snapshot(issue, user=issue.owner, delete=True) project_timeline = service.get_project_timeline(issue.project) assert project_timeline[0].event_type == "issues.issue.delete" assert project_timeline[0].data["issue"]["subject"] == "test issue timeline" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "issues.issue.delete" + assert user_watcher_timeline[0].data["issue"]["subject"] == "test issue timeline" def test_delete_task_timeline(): task = factories.TaskFactory.create(subject="test task timeline") + user_watcher= factories.UserFactory() + task.add_watcher(user_watcher) history_services.take_snapshot(task, user=task.owner, delete=True) project_timeline = service.get_project_timeline(task.project) assert project_timeline[0].event_type == "tasks.task.delete" assert project_timeline[0].data["task"]["subject"] == "test task timeline" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "tasks.task.delete" + assert user_watcher_timeline[0].data["task"]["subject"] == "test task timeline" def test_delete_wiki_page_timeline(): page = factories.WikiPageFactory.create(slug="test wiki page timeline") + user_watcher= factories.UserFactory() + page.add_watcher(user_watcher) history_services.take_snapshot(page, user=page.owner, delete=True) project_timeline = service.get_project_timeline(page.project) assert project_timeline[0].event_type == "wiki.wikipage.delete" assert project_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline" + user_watcher_timeline = service.get_profile_timeline(user_watcher) + assert user_watcher_timeline[0].event_type == "wiki.wikipage.delete" + assert user_watcher_timeline[0].data["wikipage"]["slug"] == "test wiki page timeline" def test_delete_membership_timeline(): @@ -384,16 +456,6 @@ def test_assigned_to_user_story_timeline(): assert user_timeline[0].data["userstory"]["subject"] == "test us timeline" -def test_watchers_to_user_story_timeline(): - membership = factories.MembershipFactory.create() - user_story = factories.UserStoryFactory.create(subject="test us timeline", project=membership.project) - user_story.watchers.add(membership.user) - history_services.take_snapshot(user_story, user=user_story.owner) - user_timeline = service.get_profile_timeline(membership.user) - assert user_timeline[0].event_type == "userstories.userstory.create" - assert user_timeline[0].data["userstory"]["subject"] == "test us timeline" - - def test_user_data_for_non_system_users(): user_story = factories.UserStoryFactory.create(subject="test us timeline") history_services.take_snapshot(user_story, user=user_story.owner) diff --git a/tests/integration/test_us_autoclosing.py b/tests/integration/test_us_autoclosing.py index e2205bb0..a3b43c12 100644 --- a/tests/integration/test_us_autoclosing.py +++ b/tests/integration/test_us_autoclosing.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 1898aea8..3b435f0a 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -1,14 +1,24 @@ import pytest from tempfile import NamedTemporaryFile +from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse +from django.core.files import File from .. import factories as f from taiga.base.utils import json 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.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 + +from easy_thumbnails.files import generate_all_aliases, get_thumbnailer + +import os pytestmark = pytest.mark.django_db @@ -191,6 +201,56 @@ def test_change_avatar(client): assert response.status_code == 200 +def test_change_avatar_removes_the_old_one(client): + url = reverse('users-change-avatar') + user = f.UserFactory() + + with NamedTemporaryFile(delete=False) as avatar: + avatar.write(DUMMY_BMP_DATA) + avatar.seek(0) + user.photo = File(avatar) + user.save() + generate_all_aliases(user.photo, include_global=True) + + with NamedTemporaryFile(delete=False) as avatar: + thumbnailer = get_thumbnailer(user.photo) + original_photo_paths = [user.photo.path] + original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()] + assert list(map(os.path.exists, original_photo_paths)) == [True, True, True, True] + + client.login(user) + avatar.write(DUMMY_BMP_DATA) + avatar.seek(0) + post_data = {'avatar': avatar} + response = client.post(url, post_data) + + assert response.status_code == 200 + assert list(map(os.path.exists, original_photo_paths)) == [False, False, False, False] + + +def test_remove_avatar(client): + url = reverse('users-remove-avatar') + user = f.UserFactory() + + with NamedTemporaryFile(delete=False) as avatar: + avatar.write(DUMMY_BMP_DATA) + avatar.seek(0) + user.photo = File(avatar) + user.save() + generate_all_aliases(user.photo, include_global=True) + + thumbnailer = get_thumbnailer(user.photo) + original_photo_paths = [user.photo.path] + original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()] + assert list(map(os.path.exists, original_photo_paths)) == [True, True, True, True] + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + assert list(map(os.path.exists, original_photo_paths)) == [False, False, False, False] + + def test_list_contacts_private_projects(client): project = f.ProjectFactory.create() user_1 = f.UserFactory.create() @@ -202,13 +262,13 @@ def test_list_contacts_private_projects(client): url = reverse('users-contacts', kwargs={"pk": user_1.pk}) response = client.get(url, content_type="application/json") assert response.status_code == 200 - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert len(response_content) == 0 client.login(user_1) response = client.get(url, content_type="application/json") assert response.status_code == 200 - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert len(response_content) == 1 assert response_content[0]["id"] == user_2.id @@ -227,7 +287,7 @@ def test_list_contacts_no_projects(client): response = client.get(url, content_type="application/json") assert response.status_code == 200 - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert len(response_content) == 0 @@ -246,6 +306,513 @@ def test_list_contacts_public_projects(client): response = client.get(url, content_type="application/json") assert response.status_code == 200 - response_content = json.loads(response.content.decode("utf-8")) + response_content = response.data assert len(response_content) == 1 assert response_content[0]["id"] == user_2.id + + +def test_mail_permissions(client): + user_1 = f.UserFactory.create(is_superuser=True) + user_2 = f.UserFactory.create() + + url1 = reverse('users-detail', kwargs={"pk": user_1.pk}) + url2 = reverse('users-detail', kwargs={"pk": user_2.pk}) + + # Anonymous user + response = client.json.get(url1) + assert response.status_code == 200 + assert "email" not in response.data + + response = client.json.get(url2) + assert response.status_code == 200 + assert "email" not in response.data + + # Superuser + client.login(user_1) + + response = client.json.get(url1) + assert response.status_code == 200 + assert "email" in response.data + + response = client.json.get(url2) + assert response.status_code == 200 + assert "email" in response.data + + # Normal user + client.login(user_2) + + response = client.json.get(url1) + assert response.status_code == 200 + assert "email" not in response.data + + response = client.json.get(url2) + assert response.status_code == 200 + assert "email" in response.data + + +def test_get_watched_list(): + fav_user = f.UserFactory() + viewer_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fav_user) + project.add_watcher(fav_user) + + user_story = f.UserStoryFactory(project=project, subject="Testing user story") + user_story.add_watcher(fav_user) + + task = f.TaskFactory(project=project, subject="Testing task") + task.add_watcher(fav_user) + + issue = f.IssueFactory(project=project, subject="Testing issue") + issue.add_watcher(fav_user) + + assert len(get_watched_list(fav_user, viewer_user)) == 4 + assert len(get_watched_list(fav_user, viewer_user, type="project")) == 1 + assert len(get_watched_list(fav_user, viewer_user, type="userstory")) == 1 + assert len(get_watched_list(fav_user, viewer_user, type="task")) == 1 + assert len(get_watched_list(fav_user, viewer_user, type="issue")) == 1 + assert len(get_watched_list(fav_user, viewer_user, type="unknown")) == 0 + + assert len(get_watched_list(fav_user, viewer_user, q="issue")) == 1 + assert len(get_watched_list(fav_user, viewer_user, q="unexisting text")) == 0 + + +def test_get_liked_list(): + fan_user = f.UserFactory() + viewer_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fan_user) + content_type = ContentType.objects.get_for_model(project) + f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user) + f.LikesFactory(content_type=content_type, object_id=project.id, count=1) + + assert len(get_liked_list(fan_user, viewer_user)) == 1 + assert len(get_liked_list(fan_user, viewer_user, type="project")) == 1 + assert len(get_liked_list(fan_user, viewer_user, type="unknown")) == 0 + + assert len(get_liked_list(fan_user, viewer_user, q="project")) == 1 + assert len(get_liked_list(fan_user, viewer_user, q="unexisting text")) == 0 + + +def test_get_voted_list(): + fav_user = f.UserFactory() + viewer_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fav_user) + + user_story = f.UserStoryFactory(project=project, subject="Testing user story") + content_type = ContentType.objects.get_for_model(user_story) + f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1) + + task = f.TaskFactory(project=project, subject="Testing task") + content_type = ContentType.objects.get_for_model(task) + f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=task.id, count=1) + + issue = f.IssueFactory(project=project, subject="Testing issue") + content_type = ContentType.objects.get_for_model(issue) + f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=issue.id, count=1) + + assert len(get_voted_list(fav_user, viewer_user)) == 3 + assert len(get_voted_list(fav_user, viewer_user, type="userstory")) == 1 + assert len(get_voted_list(fav_user, viewer_user, type="task")) == 1 + assert len(get_voted_list(fav_user, viewer_user, type="issue")) == 1 + assert len(get_voted_list(fav_user, viewer_user, type="unknown")) == 0 + + assert len(get_voted_list(fav_user, viewer_user, q="issue")) == 1 + assert len(get_voted_list(fav_user, viewer_user, q="unexisting text")) == 0 + + +def test_get_watched_list_valid_info_for_project(): + fav_user = f.UserFactory() + viewer_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + project.add_watcher(fav_user) + + raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0] + + project_watch_info = LikedObjectSerializer(raw_project_watch_info).data + + assert project_watch_info["type"] == "project" + assert project_watch_info["id"] == project.id + assert project_watch_info["ref"] == None + assert project_watch_info["slug"] == project.slug + assert project_watch_info["name"] == project.name + assert project_watch_info["subject"] == None + assert project_watch_info["description"] == project.description + assert project_watch_info["assigned_to"] == None + assert project_watch_info["status"] == None + assert project_watch_info["status_color"] == None + + tags_colors = {tc["name"]:tc["color"] for tc in project_watch_info["tags_colors"]} + assert "test" in tags_colors + assert "tag" in tags_colors + + assert project_watch_info["is_private"] == project.is_private + assert project_watch_info["is_fan"] == False + assert project_watch_info["is_watcher"] == False + assert project_watch_info["total_watchers"] == 1 + assert project_watch_info["total_fans"] == 0 + assert project_watch_info["project"] == None + 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["assigned_to_username"] == None + assert project_watch_info["assigned_to_full_name"] == None + assert project_watch_info["assigned_to_photo"] == None + + +def test_get_watched_list_for_project_with_ignored_notify_level(): + #If the notify policy level is ignore the project shouldn't be in the watched results + fav_user = f.UserFactory() + viewer_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fav_user) + notify_policy = NotifyPolicy.objects.get(user=fav_user, project=project) + notify_policy.notify_level=NotifyLevel.none + notify_policy.save() + + watched_list = get_watched_list(fav_user, viewer_user) + assert len(watched_list) == 0 + + +def test_get_liked_list_valid_info(): + fan_user = f.UserFactory() + viewer_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) + content_type = ContentType.objects.get_for_model(project) + like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user) + f.LikesFactory(content_type=content_type, object_id=project.id, count=1) + + raw_project_like_info = get_liked_list(fan_user, viewer_user)[0] + project_like_info = LikedObjectSerializer(raw_project_like_info).data + + assert project_like_info["type"] == "project" + assert project_like_info["id"] == project.id + assert project_like_info["ref"] == None + assert project_like_info["slug"] == project.slug + assert project_like_info["name"] == project.name + assert project_like_info["subject"] == None + assert project_like_info["description"] == project.description + assert project_like_info["assigned_to"] == None + assert project_like_info["status"] == None + assert project_like_info["status_color"] == None + + tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]} + assert "test" in tags_colors + assert "tag" in tags_colors + + assert project_like_info["is_private"] == project.is_private + + assert project_like_info["is_fan"] == False + assert project_like_info["is_watcher"] == False + assert project_like_info["total_watchers"] == 0 + assert project_like_info["total_fans"] == 1 + assert project_like_info["project"] == None + 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["assigned_to_username"] == None + assert project_like_info["assigned_to_full_name"] == None + assert project_like_info["assigned_to_photo"] == None + + +def test_get_watched_list_valid_info_for_not_project_types(): + fav_user = f.UserFactory() + viewer_user = f.UserFactory() + assigned_to_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + + factories = { + "userstory": f.UserStoryFactory, + "task": f.TaskFactory, + "issue": f.IssueFactory + } + + for object_type in factories: + instance = factories[object_type](project=project, + subject="Testing", + tags=["test1", "test2"], + assigned_to=assigned_to_user) + + instance.add_watcher(fav_user) + raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0] + instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data + + assert instance_watch_info["type"] == object_type + assert instance_watch_info["id"] == instance.id + assert instance_watch_info["ref"] == instance.ref + assert instance_watch_info["slug"] == None + assert instance_watch_info["name"] == None + assert instance_watch_info["subject"] == instance.subject + assert instance_watch_info["description"] == None + assert instance_watch_info["assigned_to"] == instance.assigned_to.id + assert instance_watch_info["status"] == instance.status.name + assert instance_watch_info["status_color"] == instance.status.color + + tags_colors = {tc["name"]:tc["color"] for tc in instance_watch_info["tags_colors"]} + assert "test1" in tags_colors + assert "test2" in tags_colors + + assert instance_watch_info["is_private"] == None + assert instance_watch_info["is_voter"] == False + assert instance_watch_info["is_watcher"] == False + assert instance_watch_info["total_watchers"] == 1 + assert instance_watch_info["total_voters"] == 0 + assert instance_watch_info["project"] == instance.project.id + 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["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"] != "" + + +def test_get_voted_list_valid_info(): + fav_user = f.UserFactory() + viewer_user = f.UserFactory() + assigned_to_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + + factories = { + "userstory": f.UserStoryFactory, + "task": f.TaskFactory, + "issue": f.IssueFactory + } + + for object_type in factories: + instance = factories[object_type](project=project, + subject="Testing", + tags=["test1", "test2"], + assigned_to=assigned_to_user) + + content_type = ContentType.objects.get_for_model(instance) + vote = f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=instance.id, count=3) + + raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0] + instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data + + assert instance_vote_info["type"] == object_type + assert instance_vote_info["id"] == instance.id + assert instance_vote_info["ref"] == instance.ref + assert instance_vote_info["slug"] == None + assert instance_vote_info["name"] == None + assert instance_vote_info["subject"] == instance.subject + assert instance_vote_info["description"] == None + assert instance_vote_info["assigned_to"] == instance.assigned_to.id + assert instance_vote_info["status"] == instance.status.name + assert instance_vote_info["status_color"] == instance.status.color + + tags_colors = {tc["name"]:tc["color"] for tc in instance_vote_info["tags_colors"]} + assert "test1" in tags_colors + assert "test2" in tags_colors + + assert instance_vote_info["is_private"] == None + assert instance_vote_info["is_voter"] == False + assert instance_vote_info["is_watcher"] == False + assert instance_vote_info["total_watchers"] == 0 + assert instance_vote_info["total_voters"] == 3 + assert instance_vote_info["project"] == instance.project.id + 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["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"] != "" + + +def test_get_watched_list_with_liked_and_voted_objects(client): + fav_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fav_user) + project.add_watcher(fav_user) + content_type = ContentType.objects.get_for_model(project) + f.LikeFactory(content_type=content_type, object_id=project.id, user=fav_user) + + voted_elements_factories = { + "userstory": f.UserStoryFactory, + "task": f.TaskFactory, + "issue": f.IssueFactory + } + + for object_type in voted_elements_factories: + instance = voted_elements_factories[object_type](project=project) + content_type = ContentType.objects.get_for_model(instance) + instance.add_watcher(fav_user) + f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user) + + client.login(fav_user) + url = reverse('users-watched', kwargs={"pk": fav_user.pk}) + response = client.get(url, content_type="application/json") + + for element_data in response.data: + #assert element_data["is_watcher"] == True + if element_data["type"] == "project": + assert element_data["is_fan"] == True + else: + assert element_data["is_voter"] == True + + +def test_get_liked_list_with_watched_objects(client): + fav_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fav_user) + project.add_watcher(fav_user) + content_type = ContentType.objects.get_for_model(project) + f.LikeFactory(content_type=content_type, object_id=project.id, user=fav_user) + + client.login(fav_user) + url = reverse('users-liked', kwargs={"pk": fav_user.pk}) + response = client.get(url, content_type="application/json") + + element_data = response.data[0] + assert element_data["is_watcher"] == True + assert element_data["is_fan"] == True + + +def test_get_voted_list_with_watched_objects(client): + fav_user = f.UserFactory() + + project = f.ProjectFactory(is_private=False, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=fav_user) + + voted_elements_factories = { + "userstory": f.UserStoryFactory, + "task": f.TaskFactory, + "issue": f.IssueFactory + } + + for object_type in voted_elements_factories: + instance = voted_elements_factories[object_type](project=project) + content_type = ContentType.objects.get_for_model(instance) + instance.add_watcher(fav_user) + f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user) + + client.login(fav_user) + url = reverse('users-voted', kwargs={"pk": fav_user.pk}) + response = client.get(url, content_type="application/json") + + for element_data in response.data: + assert element_data["is_watcher"] == True + assert element_data["is_voter"] == True + + +def test_get_watched_list_permissions(): + fav_user = f.UserFactory() + viewer_unpriviliged_user = f.UserFactory() + viewer_priviliged_user = f.UserFactory() + + project = f.ProjectFactory(is_private=True, name="Testing project") + project.add_watcher(fav_user) + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + + user_story = f.UserStoryFactory(project=project, subject="Testing user story") + user_story.add_watcher(fav_user) + + task = f.TaskFactory(project=project, subject="Testing task") + task.add_watcher(fav_user) + + issue = f.IssueFactory(project=project, subject="Testing issue") + issue.add_watcher(fav_user) + + #If the project is private a viewer user without any permission shouldn' see + # any vote + assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 0 + + #If the project is private but the viewer user has permissions the votes should + # be accesible + assert len(get_watched_list(fav_user, viewer_priviliged_user)) == 4 + + #If the project is private but has the required anon permissions the votes should + # be accesible by any user too + project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.save() + assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 4 + + +def test_get_liked_list_permissions(): + fan_user = f.UserFactory() + viewer_unpriviliged_user = f.UserFactory() + viewer_priviliged_user = f.UserFactory() + + project = f.ProjectFactory(is_private=True, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + content_type = ContentType.objects.get_for_model(project) + f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user) + f.LikesFactory(content_type=content_type, object_id=project.id, count=1) + + #If the project is private a viewer user without any permission shouldn' see + # any vote + assert len(get_liked_list(fan_user, viewer_unpriviliged_user)) == 0 + + #If the project is private but the viewer user has permissions the votes should + # be accesible + assert len(get_liked_list(fan_user, viewer_priviliged_user)) == 1 + + #If the project is private but has the required anon permissions the votes should + # be accesible by any user too + project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.save() + assert len(get_liked_list(fan_user, viewer_unpriviliged_user)) == 1 + + +def test_get_voted_list_permissions(): + fav_user = f.UserFactory() + viewer_unpriviliged_user = f.UserFactory() + viewer_priviliged_user = f.UserFactory() + + project = f.ProjectFactory(is_private=True, name="Testing project") + role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + + user_story = f.UserStoryFactory(project=project, subject="Testing user story") + content_type = ContentType.objects.get_for_model(user_story) + f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1) + + task = f.TaskFactory(project=project, subject="Testing task") + content_type = ContentType.objects.get_for_model(task) + f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=task.id, count=1) + + issue = f.IssueFactory(project=project, subject="Testing issue") + content_type = ContentType.objects.get_for_model(issue) + f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=issue.id, count=1) + + #If the project is private a viewer user without any permission shouldn' see + # any vote + assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 0 + + #If the project is private but the viewer user has permissions the votes should + # be accesible + assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 3 + + #If the project is private but has the required anon permissions the votes should + # be accesible by any user too + project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.save() + assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 3 diff --git a/tests/integration/test_userstorage_api.py b/tests/integration/test_userstorage_api.py index 1b642de2..d4050232 100644 --- a/tests/integration/test_userstorage_api.py +++ b/tests/integration/test_userstorage_api.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index add9a606..4ddaece0 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -7,6 +7,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.userstories import services, models +from taiga.projects.userstories.serializers import UserStorySerializer from .. import factories as f @@ -45,6 +46,22 @@ def test_update_userstories_order_in_bulk(): model=models.UserStory) +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) + url = reverse("userstories-list") + + data = {"subject": "Test user story", "project": project.id, "watchers": [user_watcher.id]} + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + assert response.data["watchers"] == [] + + def test_create_userstory_without_status(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -107,7 +124,7 @@ def test_api_create_in_bulk_with_status(client): assert response.data[0]["status"] == project.default_us_status.id -def test_api_update_backlog_order_in_bulk(client): +def test_api_update_orders_in_bulk(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) us1 = f.create_userstory(project=project) @@ -134,9 +151,6 @@ def test_api_update_backlog_order_in_bulk(client): assert response3.status_code == 204, response3.data -from taiga.projects.userstories.serializers import UserStorySerializer - - def test_update_userstory_points(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() @@ -152,7 +166,7 @@ def test_update_userstory_points(client): f.PointsFactory.create(project=project, value=1) points3 = f.PointsFactory.create(project=project, value=2) - us = f.UserStoryFactory.create(project=project, owner=user1) + us = f.UserStoryFactory.create(project=project, owner=user1, status__project=project, milestone__project=project) usdata = UserStorySerializer(us).data url = reverse("userstories-detail", args=[us.pk]) @@ -166,7 +180,7 @@ def test_update_userstory_points(client): data["points"].update({'2000': points3.pk}) response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 200 + assert response.status_code == 200, str(response.content) assert response.data["points"] == usdata['points'] # Api should save successful @@ -220,15 +234,15 @@ def test_archived_filter(client): data = {} response = client.get(url, data) - assert len(json.loads(response.content)) == 2 + assert len(response.data) == 2 data = {"status__is_archived": 0} response = client.get(url, data) - assert len(json.loads(response.content)) == 1 + assert len(response.data) == 1 data = {"status__is_archived": 1} response = client.get(url, data) - assert len(json.loads(response.content)) == 1 + assert len(response.data) == 1 def test_filter_by_multiple_status(client): @@ -244,10 +258,9 @@ def test_filter_by_multiple_status(client): url = reverse("userstories-list") url = "{}?status={},{}".format(reverse("userstories-list"), us1.status.id, us2.status.id) - data = {} response = client.get(url, data) - assert len(json.loads(response.content)) == 2 + assert len(response.data) == 2 def test_get_total_points(client): @@ -282,6 +295,137 @@ def test_get_total_points(client): assert us_mixed.get_total_points() == 1.0 +def test_api_filters_data(client): + project = f.ProjectFactory.create() + user1 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user1, project=project) + user2 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user2, project=project) + user3 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user3, project=project) + + status0 = f.UserStoryStatusFactory.create(project=project) + status1 = f.UserStoryStatusFactory.create(project=project) + status2 = f.UserStoryStatusFactory.create(project=project) + status3 = f.UserStoryStatusFactory.create(project=project) + + tag0 = "test1test2test3" + tag1 = "test1" + tag2 = "test2" + tag3 = "test3" + + # ------------------------------------------------------ + # | US | Owner | Assigned To | Tags | + # |-------#--------#-------------#---------------------| + # | 0 | user2 | None | tag1 | + # | 1 | user1 | None | tag2 | + # | 2 | user3 | None | tag1 tag2 | + # | 3 | user2 | None | tag3 | + # | 4 | user1 | user1 | tag1 tag2 tag3 | + # | 5 | user3 | user1 | tag3 | + # | 6 | user2 | user1 | tag1 tag2 | + # | 7 | user1 | user2 | tag3 | + # | 8 | user3 | user2 | tag1 | + # | 9 | user2 | user3 | tag0 | + # ------------------------------------------------------ + + user_story0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + user_story1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + user_story2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + user_story3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + user_story4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + user_story5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + user_story6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + user_story7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + user_story8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + user_story9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) + + url = reverse("userstories-filters-data") + "?project={}".format(project.id) + + client.login(user1) + + ## No filter + response = client.get(url) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 5 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + + ## Filter ((status0 or status3) + response = client.get(url + "&status={},{}".format(status3.id, status0.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 + + ## Filter ((tag1 and tag2) and (user1 or user2)) + response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + def test_get_invalid_csv(client): url = reverse("userstories-csv") @@ -312,6 +456,61 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[24] == attr.name + assert row[26] == attr.name row = next(reader) - assert row[24] == "val1" + assert row[26] == "val1" + + +def test_update_userstory_respecting_watchers(client): + watching_user = f.create_user() + 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=watching_user) + + client.login(user=us.owner) + url = reverse("userstories-detail", kwargs={"pk": us.pk}) + data = {"subject": "Updating test", "version": 1} + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data["subject"] == "Updating test" + assert response.data["watchers"] == [watching_user.id] + + +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=watching_user) + + client.login(user=us.owner) + url = reverse("userstories-detail", kwargs={"pk": us.pk}) + data = {"watchers": [watching_user.id], "version":1} + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data["watchers"] == [watching_user.id] + watcher_ids = list(us.get_watchers().values_list("id", flat=True)) + assert watcher_ids == [watching_user.id] + + +def test_update_userstory_remove_watchers(client): + watching_user = f.create_user() + 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=watching_user) + + client.login(user=us.owner) + url = reverse("userstories-detail", kwargs={"pk": us.pk}) + data = {"watchers": [], "version":1} + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data["watchers"] == [] + watcher_ids = list(us.get_watchers().values_list("id", flat=True)) + assert watcher_ids == [] diff --git a/tests/integration/test_vote_issues.py b/tests/integration/test_vote_issues.py index 691ce432..9b880d36 100644 --- a/tests/integration/test_vote_issues.py +++ b/tests/integration/test_vote_issues.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -51,8 +51,8 @@ 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) - url = reverse("issue-voters-list", args=(issue.id,)) f.VoteFactory.create(content_object=issue, user=user) + url = reverse("issue-voters-list", args=(issue.id,)) client.login(user) response = client.get(url) @@ -60,7 +60,6 @@ def test_list_issue_voters(client): assert response.status_code == 200 assert response.data[0]['id'] == user.id - def test_get_issue_voter(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) @@ -74,7 +73,6 @@ def test_get_issue_voter(client): assert response.status_code == 200 assert response.data['id'] == vote.user.id - def test_get_issue_votes(client): user = f.UserFactory.create() issue = f.create_issue(owner=user) @@ -87,4 +85,37 @@ def test_get_issue_votes(client): response = client.get(url) assert response.status_code == 200 - assert response.data['votes'] == 5 + assert response.data['total_voters'] == 5 + + +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.VotesFactory.create(content_object=issue) + url_detail = reverse("issues-detail", args=(issue.id,)) + url_upvote = reverse("issues-upvote", args=(issue.id,)) + url_downvote = reverse("issues-downvote", args=(issue.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 0 + assert response.data['is_voter'] == False + + response = client.post(url_upvote) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 1 + assert response.data['is_voter'] == True + + response = client.post(url_downvote) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 0 + assert response.data['is_voter'] == False diff --git a/tests/integration/test_vote_tasks.py b/tests/integration/test_vote_tasks.py new file mode 100644 index 00000000..8b1a3605 --- /dev/null +++ b/tests/integration/test_vote_tasks.py @@ -0,0 +1,123 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("tasks-upvote", args=(task.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("tasks-downvote", args=(task.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.VoteFactory.create(content_object=task, user=user) + url = reverse("task-voters-list", args=(task.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + vote = f.VoteFactory.create(content_object=task, user=user) + url = reverse("task-voters-detail", args=(task.id, vote.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == vote.user.id + + +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) + url = reverse("tasks-detail", args=(task.id,)) + + f.VotesFactory.create(content_object=task, count=5) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['total_voters'] == 5 + + +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) + f.VotesFactory.create(content_object=task) + url_detail = reverse("tasks-detail", args=(task.id,)) + url_upvote = reverse("tasks-upvote", args=(task.id,)) + url_downvote = reverse("tasks-downvote", args=(task.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 0 + assert response.data['is_voter'] == False + + response = client.post(url_upvote) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 1 + assert response.data['is_voter'] == True + + response = client.post(url_downvote) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 0 + assert response.data['is_voter'] == False diff --git a/tests/integration/test_vote_userstories.py b/tests/integration/test_vote_userstories.py new file mode 100644 index 00000000..ae118db1 --- /dev/null +++ b/tests/integration/test_vote_userstories.py @@ -0,0 +1,122 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("userstories-upvote", args=(user_story.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("userstories-downvote", args=(user_story.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.VoteFactory.create(content_object=user_story, user=user) + url = reverse("userstory-voters-list", args=(user_story.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + +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) + vote = f.VoteFactory.create(content_object=user_story, user=user) + url = reverse("userstory-voters-detail", args=(user_story.id, vote.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == vote.user.id + + +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) + url = reverse("userstories-detail", args=(user_story.id,)) + + f.VotesFactory.create(content_object=user_story, count=5) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['total_voters'] == 5 + + +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) + 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,)) + url_downvote = reverse("userstories-downvote", args=(user_story.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 0 + assert response.data['is_voter'] == False + + response = client.post(url_upvote) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 1 + assert response.data['is_voter'] == True + + response = client.post(url_downvote) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_voters'] == 0 + assert response.data['is_voter'] == False diff --git a/tests/integration/test_votes.py b/tests/integration/test_votes.py index cb7f3660..0408529e 100644 --- a/tests/integration/test_votes.py +++ b/tests/integration/test_votes.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py new file mode 100644 index 00000000..36ac157e --- /dev/null +++ b/tests/integration/test_watch_issues.py @@ -0,0 +1,149 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import json +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("issues-watch", args=(issue.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("issues-watch", args=(issue.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.WatchedFactory.create(content_object=issue, user=user) + url = reverse("issue-watchers-list", args=(issue.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + watch = f.WatchedFactory.create(content_object=issue, user=user) + url = reverse("issue-watchers-detail", args=(issue.id, watch.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == watch.user.id + + +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) + url = reverse("issues-detail", args=(issue.id,)) + + f.WatchedFactory.create(content_object=issue, user=user) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['watchers'] == [user.id] + assert response.data['total_watchers'] == 1 + + +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) + url_detail = reverse("issues-detail", args=(issue.id,)) + url_watch = reverse("issues-watch", args=(issue.id,)) + url_unwatch = reverse("issues-unwatch", args=(issue.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False + + response = client.post(url_watch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [user.id] + assert response.data['is_watcher'] == True + + response = client.post(url_unwatch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False + + +def test_remove_issue_watcher(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create() + issue = f.IssueFactory(project=project, + status__project=project, + severity__project=project, + priority__project=project, + type__project=project, + milestone__project=project) + + issue.add_watcher(user) + role = f.RoleFactory.create(project=project, permissions=['modify_issue', 'view_issues']) + f.MembershipFactory.create(project=project, user=user, role=role) + + url = reverse("issues-detail", args=(issue.id,)) + + client.login(user) + + data = {"version": issue.version, "watchers": []} + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False diff --git a/tests/integration/test_watch_milestones.py b/tests/integration/test_watch_milestones.py new file mode 100644 index 00000000..acbbe85a --- /dev/null +++ b/tests/integration/test_watch_milestones.py @@ -0,0 +1,123 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import json +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("milestones-watch", args=(milestone.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("milestones-watch", args=(milestone.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.WatchedFactory.create(content_object=milestone, user=user) + url = reverse("milestone-watchers-list", args=(milestone.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + watch = f.WatchedFactory.create(content_object=milestone, user=user) + url = reverse("milestone-watchers-detail", args=(milestone.id, watch.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == watch.user.id + + +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) + url = reverse("milestones-detail", args=(milestone.id,)) + + f.WatchedFactory.create(content_object=milestone, user=user) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['total_watchers'] == 1 + + +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) + url_detail = reverse("milestones-detail", args=(milestone.id,)) + url_watch = reverse("milestones-watch", args=(milestone.id,)) + url_unwatch = reverse("milestones-unwatch", args=(milestone.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 0 + assert response.data['is_watcher'] == False + + response = client.post(url_watch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 1 + assert response.data['is_watcher'] == True + + response = client.post(url_unwatch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 0 + assert response.data['is_watcher'] == False diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py new file mode 100644 index 00000000..fd1c9560 --- /dev/null +++ b/tests/integration/test_watch_projects.py @@ -0,0 +1,160 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import json +from django.core.urlresolvers import reverse + +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS + +from .. import factories as f + +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) + url = reverse("projects-watch", args=(project.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("projects-watch", args=(project.id,)) + + client.login(user) + data = { + "notify_level": 1 + } + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + +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) + url = reverse("projects-watch", args=(project.id,)) + + client.login(user) + data = { + "notify_level": 333 + } + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert response.data["_error_message"] == "Invalid value for notify level" + + +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) + url = reverse("projects-unwatch", args=(project.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.WatchedFactory.create(content_object=project, user=user) + url = reverse("project-watchers-list", args=(project.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + watch = f.WatchedFactory.create(content_object=project, user=user) + url = reverse("project-watchers-detail", args=(project.id, watch.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == watch.user.id + + +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) + url = reverse("projects-detail", args=(project.id,)) + + f.WatchedFactory.create(content_object=project, user=user) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['total_watchers'] == 1 + + +def test_get_project_is_watcher(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS))) + + url_detail = reverse("projects-detail", args=(project.id,)) + url_watch = reverse("projects-watch", args=(project.id,)) + url_unwatch = reverse("projects-unwatch", args=(project.id,)) + + client.login(user) + + response = client.get(url_detail) + + assert response.status_code == 200 + assert response.data['total_watchers'] == 0 + + assert response.data['is_watcher'] == False + + response = client.post(url_watch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 1 + assert response.data['is_watcher'] == True + + response = client.post(url_unwatch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 0 + assert response.data['is_watcher'] == False diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py new file mode 100644 index 00000000..03b70190 --- /dev/null +++ b/tests/integration/test_watch_tasks.py @@ -0,0 +1,147 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import json +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("tasks-watch", args=(task.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("tasks-watch", args=(task.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.WatchedFactory.create(content_object=task, user=user) + url = reverse("task-watchers-list", args=(task.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + watch = f.WatchedFactory.create(content_object=task, user=user) + url = reverse("task-watchers-detail", args=(task.id, watch.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == watch.user.id + + +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) + url = reverse("tasks-detail", args=(task.id,)) + + f.WatchedFactory.create(content_object=task, user=user) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['watchers'] == [user.id] + assert response.data['total_watchers'] == 1 + + +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) + url_detail = reverse("tasks-detail", args=(task.id,)) + url_watch = reverse("tasks-watch", args=(task.id,)) + url_unwatch = reverse("tasks-unwatch", args=(task.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False + + response = client.post(url_watch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [user.id] + assert response.data['is_watcher'] == True + + response = client.post(url_unwatch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False + + +def test_remove_task_watcher(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create() + task = f.TaskFactory(project=project, + user_story=None, + status__project=project, + milestone__project=project) + + task.add_watcher(user) + role = f.RoleFactory.create(project=project, permissions=['modify_task', 'view_tasks']) + f.MembershipFactory.create(project=project, user=user, role=role) + + url = reverse("tasks-detail", args=(task.id,)) + + client.login(user) + + data = {"version": task.version, "watchers": []} + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py new file mode 100644 index 00000000..aaf797b8 --- /dev/null +++ b/tests/integration/test_watch_userstories.py @@ -0,0 +1,146 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import json +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("userstories-watch", args=(user_story.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("userstories-unwatch", args=(user_story.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.WatchedFactory.create(content_object=user_story, user=user) + url = reverse("userstory-watchers-list", args=(user_story.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + watch = f.WatchedFactory.create(content_object=user_story, user=user) + url = reverse("userstory-watchers-detail", args=(user_story.id, watch.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == watch.user.id + + +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) + url = reverse("userstories-detail", args=(user_story.id,)) + + f.WatchedFactory.create(content_object=user_story, user=user) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['watchers'] == [user.id] + assert response.data['total_watchers'] == 1 + + +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) + 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,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False + + response = client.post(url_watch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [user.id] + assert response.data['is_watcher'] == True + + response = client.post(url_unwatch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False + + +def test_remove_user_story_watcher(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create() + us = f.UserStoryFactory(project=project, + status__project=project, + milestone__project=project) + + us.add_watcher(user) + role = f.RoleFactory.create(project=project, permissions=['modify_us', 'view_us']) + f.MembershipFactory.create(project=project, user=user, role=role) + + url = reverse("userstories-detail", args=(us.id,)) + + client.login(user) + + data = {"version": us.version, "watchers": []} + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data['watchers'] == [] + assert response.data['is_watcher'] == False diff --git a/tests/integration/test_watch_wikipages.py b/tests/integration/test_watch_wikipages.py new file mode 100644 index 00000000..e90e2cd1 --- /dev/null +++ b/tests/integration/test_watch_wikipages.py @@ -0,0 +1,123 @@ +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest +import json +from django.core.urlresolvers import reverse + +from .. import factories as f + +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) + url = reverse("wiki-watch", args=(wikipage.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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) + url = reverse("wiki-watch", args=(wikipage.id,)) + + client.login(user) + response = client.post(url) + + assert response.status_code == 200 + + +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.WatchedFactory.create(content_object=wikipage, user=user) + url = reverse("wiki-watchers-list", args=(wikipage.id,)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data[0]['id'] == user.id + + +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) + watch = f.WatchedFactory.create(content_object=wikipage, user=user) + url = reverse("wiki-watchers-detail", args=(wikipage.id, watch.user.id)) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['id'] == watch.user.id + + +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) + url = reverse("wiki-detail", args=(wikipage.id,)) + + f.WatchedFactory.create(content_object=wikipage, user=user) + + client.login(user) + response = client.get(url) + + assert response.status_code == 200 + assert response.data['total_watchers'] == 1 + + +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) + url_detail = reverse("wiki-detail", args=(wikipage.id,)) + url_watch = reverse("wiki-watch", args=(wikipage.id,)) + url_unwatch = reverse("wiki-unwatch", args=(wikipage.id,)) + + client.login(user) + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 0 + assert response.data['is_watcher'] == False + + response = client.post(url_watch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 1 + assert response.data['is_watcher'] == True + + response = client.post(url_unwatch) + assert response.status_code == 200 + + response = client.get(url_detail) + assert response.status_code == 200 + assert response.data['total_watchers'] == 0 + assert response.data['is_watcher'] == False diff --git a/tests/integration/test_webhooks.py b/tests/integration/test_webhooks.py index 0b3b32f0..4e33b919 100644 --- a/tests/integration/test_webhooks.py +++ b/tests/integration/test_webhooks.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -90,3 +90,26 @@ def test_new_object_with_two_webhook(settings): with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock: services.take_snapshot(obj, user=obj.owner, comment="test", delete=True) assert delete_webhook_mock.call_count == 2 + + +def test_send_request_one_webhook(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + + objects = [ + f.IssueFactory.create(project=project), + f.TaskFactory.create(project=project), + f.UserStoryFactory.create(project=project), + f.WikiPageFactory.create(project=project) + ] + + for obj in objects: + with patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test") + assert _send_request_mock.call_count == 1 + + for obj in objects: + with patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test", delete=True) + assert _send_request_mock.call_count == 1 diff --git a/tests/models.py b/tests/models.py index fe21e87c..87c667dc 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e1159b0c..ac4f395d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/unit/test_base_api_permissions.py b/tests/unit/test_base_api_permissions.py index dd736808..e0fc748c 100644 --- a/tests/unit/test_base_api_permissions.py +++ b/tests/unit/test_base_api_permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/unit/test_deferred.py b/tests/unit/test_deferred.py index c4e076c4..46afe862 100644 --- a/tests/unit/test_deferred.py +++ b/tests/unit/test_deferred.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py index 0a6ba9d6..17564fa7 100644 --- a/tests/unit/test_export.py +++ b/tests/unit/test_export.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 @@ -28,7 +28,6 @@ def test_export_issue_finish_date(client): issue = f.IssueFactory.create(finished_date="2014-10-22") output = io.StringIO() render_project(issue.project, output) - print(output.getvalue()) project_data = json.loads(output.getvalue()) finish_date = project_data["issues"][0]["finished_date"] assert finish_date == "2014-10-22T00:00:00+0000" diff --git a/tests/unit/test_gravatar.py b/tests/unit/test_gravatar.py index d91a90b2..ba1d18be 100644 --- a/tests/unit/test_gravatar.py +++ b/tests/unit/test_gravatar.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/unit/test_mdrender.py b/tests/unit/test_mdrender.py index 1de084d8..a58e5cca 100644 --- a/tests/unit/test_mdrender.py +++ b/tests/unit/test_mdrender.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -165,8 +165,8 @@ def test_render_relative_image(): def test_render_triple_quote_code(): - expected_result = "
print(\"test\")\n
" - assert render(dummy_project, "```\nprint(\"test\")\n```") == expected_result + expected_result = "
print(\"test\")\n
" + assert render(dummy_project, "```python\nprint(\"test\")\n```") == expected_result def test_render_triple_quote_and_lang_code(): diff --git a/tests/unit/test_permissions.py b/tests/unit/test_permissions.py index c1a0e765..b4c0ae45 100644 --- a/tests/unit/test_permissions.py +++ b/tests/unit/test_permissions.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/unit/test_slug.py b/tests/unit/test_slug.py index e687bcbd..65fb4cf0 100644 --- a/tests/unit/test_slug.py +++ b/tests/unit/test_slug.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/unit/test_timeline.py b/tests/unit/test_timeline.py index 377e9728..7e8ff24b 100644 --- a/tests/unit/test_timeline.py +++ b/tests/unit/test_timeline.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/unit/test_tokens.py b/tests/unit/test_tokens.py index 1da085e1..ca2b09b2 100644 --- a/tests/unit/test_tokens.py +++ b/tests/unit/test_tokens.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 0bb5922a..9521974b 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,6 +1,6 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán # 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 diff --git a/tests/utils.py b/tests/utils.py index 10f1fd9f..389d9c59 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino +# Copyright (C) 2014-2015 David Barragán +# Copyright (C) 2014-2015 Anler Hernández # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -16,7 +16,6 @@ # along with this program. If not, see . from django.db.models import signals -from taiga.base.utils import json def signals_switch(): @@ -50,8 +49,8 @@ def _helper_test_http_method_responses(client, method, url, data, users, after_e response = getattr(client, method)(url, data, content_type=content_type) else: response = getattr(client, method)(url) - if response.status_code >= 400: - print("Response content:", response.content) + #if response.status_code >= 400: + # print("Response content:", response.content) results.append(response) @@ -69,9 +68,9 @@ def helper_test_http_method(client, method, url, data, users, after_each_request def helper_test_http_method_and_count(client, method, url, data, users, after_each_request=None): responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request) - return list(map(lambda r: (r.status_code, len(json.loads(r.content.decode('utf-8')))), responses)) + return list(map(lambda r: (r.status_code, len(r.data) if isinstance(r.data, list) and 200 <= r.status_code < 300 else 0), responses)) def helper_test_http_method_and_keys(client, method, url, data, users, after_each_request=None): responses = _helper_test_http_method_responses(client, method, url, data, users, after_each_request) - return list(map(lambda r: (r.status_code, set(json.loads(r.content.decode('utf-8')).keys())), responses)) + return list(map(lambda r: (r.status_code, set(r.data.keys() if isinstance(r.data, dict) and 200 <= r.status_code < 300 else [])), responses))