Compare commits

..

84 Commits

Author SHA1 Message Date
hecfernandez 120442206b fix tags exclude filter behaviour (#1246)
* fix tags exclude filter behaviour
2019-02-13 14:20:04 +01:00
Álex Hermida d6d3f8c6a8 Update change log 2019-02-01 21:54:16 +01:00
Miguel Gonzalez 3cebad87eb fix: Sanitize email header removing linefeed and carriage return chars 2019-01-29 13:45:23 +01:00
Álex Hermida f45f5ae08a Activate Ukrainian language 2019-01-29 13:15:19 +01:00
Álex Hermida 0434e8b78b Fix calculate_milestone_is_closed and add tests 2019-01-29 10:30:59 +01:00
Héctor Fernández Cascallar 0c0e09819a refactor concatenation method for keep compatibility 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 2b38fefa13 refactor exclude filter mode implementation 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 7c5ba16d24 refactor prepare filter methods in tags filter 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar b0d065167c refactor tags exclude filter implementation 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 4bb12d73d9 fix membership query for avoid project-role without users 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar e736846562 code refactor 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 2bdd652ea7 remove pytest development mark 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 483d3ffd5f add tests for task filters 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 2d77f8974b add exclude mode for user stories filter 2019-01-22 11:29:20 +01:00
Héctor Fernández Cascallar 77fa09a953 add exclude mode for issues filters 2019-01-22 11:29:20 +01:00
Álex Hermida df9830bb4f Update change log 2019-01-14 13:48:44 +01:00
Álex Hermida 849ce97a1c Update messages catalog 2019-01-14 13:25:24 +01:00
Álex Hermida c260a4dd22 Upgrade calculate milestone is closed 2019-01-11 13:24:48 +01:00
Álex Hermida 5f301450df Modify create task factory 2019-01-03 22:13:49 +01:00
Álex Hermida abf2b11220 Refactor and fix tests 2019-01-03 22:13:49 +01:00
Álex Hermida a4256c3f09 Add issue milestones signals 2019-01-03 22:13:49 +01:00
Álex Hermida 18e97be27c Add move issues to sprint test & endpoint 2019-01-03 22:13:49 +01:00
Álex Hermida a17ed83755 Move tasks to another sprint 2019-01-03 22:13:49 +01:00
Álex Hermida fe4cddac30 Test move and close oprevious sprint 2019-01-03 22:13:49 +01:00
Álex Hermida 0cb423c929 Change approach move us's from sprint to another 2019-01-03 22:13:49 +01:00
Álex Hermida 39e9de71cf Add milestones bulk items 2019-01-03 22:13:49 +01:00
Álex Hermida 4c74e6182f Add test test_api_update_milestone_in_bulk_userstories 2019-01-03 22:13:49 +01:00
Álex Hermida dae83618a1 Update change log 2018-12-10 19:36:53 +01:00
Miguel Gonzalez 97b69cdb61 Add extra requirements for oauthlib
See https://oauthlib.readthedocs.io/en/latest/faq.html?highlight=signedtoken#oauth-2-serviceapplicationclient-and-oauth-1-with-rsa-sha1-signatures-say-could-not-import-jwt-what-should-i-do
2018-12-10 10:49:58 +01:00
Álex Hermida a5386cb79c Update change log 2018-12-03 21:41:03 +01:00
Álex Hermida 6a0a55f982 Update messages catalog 2018-12-03 21:40:15 +01:00
Miguel Gonzalez 2d91b75096 Update changelog 2018-11-28 08:34:56 +01:00
Álex Hermida 0e1105fb2b Check permissions on webnotifications list 2018-11-28 08:32:33 +01:00
Álex Hermida 49520bcc9b Update change log 2018-11-27 18:27:15 +01:00
Álex Hermida 3cd0da80d5 Update messages catalog 2018-11-27 18:09:47 +01:00
Álex Hermida 4fa4c02d16 Add my_homepage attr to projects list 2018-11-27 18:03:50 +01:00
Daniel García 809a90b777 Added custom fields dropdown, checkbox and number 2018-11-27 18:03:50 +01:00
Álex Hermida 305b8f048f Add tests update invalid milestone in bulk 2018-11-27 18:03:50 +01:00
Álex Hermida ad757f233b Add update issue milestone in bulk 2018-11-27 18:03:50 +01:00
Álex Hermida 8806fce1eb Add test update issue milestone in bulk 2018-11-27 18:03:50 +01:00
Álex Hermida 7d8073f801 Add update tasks milestone in bulk 2018-11-27 18:03:50 +01:00
Álex Hermida 90a12ee95a Add test update tasks milestone in bulk 2018-11-27 18:03:50 +01:00
Álex Hermida 10b5baefbb Add user story bulk update milestone test 2018-11-27 18:03:50 +01:00
Álex Hermida 80002086d5 Fixe timeline query 2018-11-27 18:03:50 +01:00
Daniel García 9b70d25f49 Enable filtering by project in UserProjectSettingsViewSet 2018-11-27 18:03:50 +01:00
Miguel Gonzalez 8e4d1a5653 Pin kombu version to match celery
See https://github.com/celery/kombu/issues/873
2018-11-27 18:03:50 +01:00
Daniel García a35db80932 Set project on top on user projects list 2018-11-27 18:03:50 +01:00
Miguel Gonzalez 533d72fee6 Remove unused file 2018-11-27 18:03:50 +01:00
Miguel Gonzalez 117fc011f7 Update compatibility to python3.5+ 2018-11-27 18:03:50 +01:00
Miguel Gonzalez 963763be31 Adapt test to new bleach version behaviour 2018-11-27 18:03:50 +01:00
Miguel Gonzalez 22eae180c6 Remove md_globals argument
See https://python-markdown.github.io/change_log/release-3.0/#md_globals-keyword-deprecated-from-extension-api
2018-11-27 18:03:50 +01:00
Miguel Gonzalez 6842a7bb21 Configure tox 2018-11-27 18:03:50 +01:00
Miguel Gonzalez 03bf1f0a9a Adjust config 2018-11-27 18:03:50 +01:00
Miguel Gonzalez cb24850466 Update requirements and use pipenv to manage them 2018-11-27 18:03:50 +01:00
Álex Hermida 203e37d98b add attachment new filter to only relevant timeline 2018-11-27 18:03:50 +01:00
Daniel García b47a4d5dad Enable activity pagination 2018-11-27 18:03:50 +01:00
Daniel García 9fd40ce1bc Set default permissions for anon user when importing from Trello 2018-11-27 18:03:50 +01:00
Daniel García 08e31a2ca9 Web notifications 2018-11-27 18:03:50 +01:00
Álex Hermida 4fe58359f4 Refactor tests 2018-11-27 18:03:50 +01:00
Álex Hermida 4b5e58f806 Refactor migrations 2018-11-27 18:03:50 +01:00
Álex Hermida 81ff7fd666 remove extra sections 2018-11-27 18:03:50 +01:00
Álex Hermida fc9af7d1df Remove meetup from homepage section 2018-11-27 18:03:50 +01:00
Álex Hermida 54fe5ba79c Validate user settings homepage 2018-11-27 18:03:50 +01:00
Álex Hermida 5ac2cf6146 Add allowed sections to project settings serializer 2018-11-27 18:03:50 +01:00
Álex Hermida 2d7229e494 Add homepage allowed sections 2018-11-27 18:03:50 +01:00
Álex Hermida f188974f31 Add project settings tests 2018-11-27 18:03:50 +01:00
Álex Hermida 0512b1a47d Add migrations 2018-11-27 18:03:50 +01:00
Daniel García 9389b65157 User settings by project 2018-11-27 18:03:50 +01:00
Álex Hermida 8bfdf596dc Update change log 2018-10-14 19:34:00 +02:00
Álex Hermida 8fbe0c78d5 Update messages catalog 2018-10-14 19:32:03 +02:00
Álex Hermida 09f8ada4c2 Refactor remove webhooks logs 2018-10-14 19:20:56 +02:00
Álex Hermida 9aaf45c725 Update po files 2018-10-14 19:20:56 +02:00
Álex Hermida e37eb65431 Add webhook block private setting 2018-10-14 19:20:56 +02:00
Álex Hermida 8b130f0361 Refactor old tests 2018-10-14 19:20:56 +02:00
Álex Hermida 39f3f82970 Add tests 2018-10-14 19:20:56 +02:00
Álex Hermida 178ab9ec43 Prevent local webhook requests 2018-10-14 19:20:56 +02:00
Álex Hermida 221211e716 Add url validation 2018-10-14 19:20:56 +02:00
Álex Hermida 1a850f5179 Update change log 2018-09-19 12:50:46 +02:00
Álex Hermida 34c5bd40d3 Save thumbnail file path in timeline and use it in serializer 2018-09-19 12:13:38 +02:00
Álex Hermida d55ee21e8e Remove thumb_url in history attachment freeze data 2018-09-19 12:13:38 +02:00
Álex Hermida ef8f44e434 Refactor thumb_url in timeline serializer 2018-09-19 12:13:38 +02:00
Álex Hermida c40cb86dbd Refactor get_thumbnail_url 2018-09-19 12:13:38 +02:00
Daniel García e7884e417e Enable ordering us tasks list by us_order 2018-09-19 09:25:47 +02:00
Miguel Gonzalez a19bec8ccd Fix non existing key case 2018-09-19 07:23:00 +02:00
121 changed files with 9241 additions and 710 deletions

View File

@ -17,8 +17,9 @@ before_install:
- sudo /etc/init.d/postgresql start 9.4
- psql -c 'create database taiga;' -U postgres
install:
- travis_retry pip install -r requirements-devel.txt
- travis_retry pip install pipenv
- travis_retry pipenv sync --dev
script:
- travis_retry coverage run --source=taiga --omit='*tests*,*commands*,*migrations*,*admin*,*.jinja,*dashboard*,*settings*,*wsgi*,*questions*,*documents*' -m py.test -v --tb=native
- travis_retry pipenv run coverage run --source=taiga --omit='*tests*,*commands*,*migrations*,*admin*,*.jinja,*dashboard*,*settings*,*wsgi*,*questions*,*documents*' -m pytest -v --tb=native
after_success:
- coveralls

View File

@ -2,6 +2,72 @@
## Unreleased
## 4.1.0 (2019-02-04)
### Misc
- Fix Close sprints
### Features:
- Negative filters
- Activate the Ukrainian language
## 4.0.4 (2019-01-15)
### Misc
- Minor bug fixes.
## 4.0.3 (2018-12-11)
### Misc
- Add extra requirements for oauthlib
## 4.0.2 (2018-12-04)
### Misc
- Update messages catalog.
## 4.0.1 (2018-11-28)
### Misc
- Minor bug fix.
## 4.0.0 Larix cajanderi (2018-11-28)
### Features
- Custom home section (https://tree.taiga.io/project/taiga/issue/3059)
- Custom fields (https://tree.taiga.io/project/taiga/issue/3725):
- Dropdown
- Checkbox
- Number
- Bulk move unfinished objects in sprint (https://tree.taiga.io/project/taiga/issue/5451)
- Paginate history activity
- Improve notifications area (https://tree.taiga.io/project/taiga/issue/2165 and
https://tree.taiga.io/project/taiga/issue/3752)
### Misc
- Minor icon changes
- Lots of small bugfixes
## 3.4.5 (2018-10-15)
### Features
- Prevent local Webhooks
## 3.4.4 (2018-09-19)
### Misc
- Small fixes
## 3.4.3 (2018-09-19)
### Misc

53
Pipfile Normal file
View File

@ -0,0 +1,53 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
asana = "==0.6.7"
bleach = "==2.1.4"
celery = "==4.0.2"
cryptography = "==2.3.1"
diff-match-patch = "==20121119"
django-ipware = "==1.1.6"
django-jinja = "==2.3.1"
django-picklefield = "==0.3.2"
django-pglocks = "==1.0.2"
django-sampledatahelper = "==0.4.1"
django-sites = "==0.9"
django-sr = "==0.0.4"
djmail = "==1.0.1"
easy-thumbnails = "==2.4.1"
fn = "==0.4.3"
gunicorn = "==19.7.1"
kombu = "==4.0.2" # The same as celery
netaddr = "==0.7.19"
premailer = "==3.0.1"
psd-tools = "==1.4"
"psycopg2-binary" = "==2.7.5"
python-dateutil = "==2.7.5"
python-magic = "==0.4.13"
pytz = "*"
raven = "==6.1.0"
redis = "==2.10.5"
requests = "==2.20.0"
requests_oauthlib = "*"
serpy = "==0.1.1"
webcolors = "==1.7"
CairoSVG = "==2.0.3"
Django = "~=1.11.15"
Markdown = "==3.0.1"
Pillow = "==4.1.1"
Unidecode = "==0.4.20"
Pygments = "==2.2.0"
oauthlib = {extras = ["signedtoken"],version = "*"}
[dev-packages]
coverage = "*"
coveralls = "*"
pytest = "*"
pytest-django = "*"
factory-boy = "*"
[requires]
python_version = "3.6"

805
Pipfile.lock generated Normal file
View File

@ -0,0 +1,805 @@
{
"_meta": {
"hash": {
"sha256": "2505838cdc4b5390130990ae63647a4447c5f146ab37ca8bf29f627091112cf7"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"amqp": {
"hashes": [
"sha256:073dd02fdd73041bffc913b767866015147b61f2a9bc104daef172fc1a0066eb",
"sha256:eed41946890cd43e8dee44a316b85cf6fee5a1a34bb4a562b660a358eb529e1b"
],
"version": "==2.3.2"
},
"asana": {
"hashes": [
"sha256:412398ff0f72104f5fb602653b994cdd36de89aedff2d37229b237f300f5f01b",
"sha256:d576601116764050c4cf63b417f1c24700b76cf6686f0e51e6b0b77d450e7973"
],
"index": "pypi",
"version": "==0.6.7"
},
"asn1crypto": {
"hashes": [
"sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
"sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
],
"version": "==0.24.0"
},
"billiard": {
"hashes": [
"sha256:42d9a227401ac4fba892918bba0a0c409def5435c4b483267ebfe821afaaba0e"
],
"version": "==3.5.0.5"
},
"bleach": {
"hashes": [
"sha256:0ee95f6167129859c5dce9b1ca291ebdb5d8cd7e382ca0e237dfd0dad63f63d8",
"sha256:24754b9a7d530bf30ce7cbc805bc6cce785660b4a10ff3a43633728438c105ab"
],
"index": "pypi",
"version": "==2.1.4"
},
"cairocffi": {
"hashes": [
"sha256:15386c3a9e08823d6826c4491eaccc7b7254b1dc587a3b9ce60c350c3f990337"
],
"version": "==0.9.0"
},
"cairosvg": {
"hashes": [
"sha256:d2da5aaa31ded26affd5cdffc371ec4cc48800bc2d822a9c28504360482418a1"
],
"index": "pypi",
"version": "==2.0.3"
},
"celery": {
"hashes": [
"sha256:0e5b7e0d7f03aa02061abfd27aa9da05b6740281ca1f5228a54fbf7fe74d8afa",
"sha256:e3d5a6c56a73ff8f2ddd4d06dc37f4c2afe4bb4da7928b884d0725ea865ef54d"
],
"index": "pypi",
"version": "==4.0.2"
},
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"cffi": {
"hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"version": "==1.11.5"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"contextlib2": {
"hashes": [
"sha256:509f9419ee91cdd00ba34443217d5ca51f5a364a404e1dce9e8979cea969ca48",
"sha256:f5260a6e679d2ff42ec91ec5252f4eeffdcf21053db9113bd0a8e4d953769c00"
],
"version": "==0.5.5"
},
"cryptography": {
"hashes": [
"sha256:02602e1672b62e803e08617ec286041cc453e8d43f093a5f4162095506bc0beb",
"sha256:10b48e848e1edb93c1d3b797c83c72b4c387ab0eb4330aaa26da8049a6cbede0",
"sha256:17db09db9d7c5de130023657be42689d1a5f60502a14f6f745f6f65a6b8195c0",
"sha256:227da3a896df1106b1a69b1e319dce218fa04395e8cc78be7e31ca94c21254bc",
"sha256:2cbaa03ac677db6c821dac3f4cdfd1461a32d0615847eedbb0df54bb7802e1f7",
"sha256:31db8febfc768e4b4bd826750a70c79c99ea423f4697d1dab764eb9f9f849519",
"sha256:4a510d268e55e2e067715d728e4ca6cd26a8e9f1f3d174faf88e6f2cb6b6c395",
"sha256:6a88d9004310a198c474d8a822ee96a6dd6c01efe66facdf17cb692512ae5bc0",
"sha256:76936ec70a9b72eb8c58314c38c55a0336a2b36de0c7ee8fb874a4547cadbd39",
"sha256:7e3b4aecc4040928efa8a7cdaf074e868af32c58ffc9bb77e7bf2c1a16783286",
"sha256:8168bcb08403ef144ff1fb880d416f49e2728101d02aaadfe9645883222c0aa5",
"sha256:8229ceb79a1792823d87779959184a1bf95768e9248c93ae9f97c7a2f60376a1",
"sha256:8a19e9f2fe69f6a44a5c156968d9fc8df56d09798d0c6a34ccc373bb186cee86",
"sha256:8d10113ca826a4c29d5b85b2c4e045ffa8bad74fb525ee0eceb1d38d4c70dfd6",
"sha256:be495b8ec5a939a7605274b6e59fbc35e76f5ad814ae010eb679529671c9e119",
"sha256:dc2d3f3b1548f4d11786616cf0f4415e25b0fbecb8a1d2cd8c07568f13fdde38",
"sha256:e4aecdd9d5a3d06c337894c9a6e2961898d3f64fe54ca920a72234a3de0f9cb3",
"sha256:e79ab4485b99eacb2166f3212218dd858258f374855e1568f728462b0e6ee0d9",
"sha256:f995d3667301e1754c57b04e0bae6f0fa9d710697a9f8d6712e8cca02550910f"
],
"index": "pypi",
"version": "==2.3.1"
},
"cssselect": {
"hashes": [
"sha256:066d8bc5229af09617e24b3ca4d52f1f9092d9e061931f4184cd572885c23204",
"sha256:3b5103e8789da9e936a68d993b70df732d06b8bb9a337a05ed4eb52c17ef7206"
],
"version": "==1.0.3"
},
"cssutils": {
"hashes": [
"sha256:a2fcf06467553038e98fea9cfe36af2bf14063eb147a70958cfcaa8f5786acaf",
"sha256:c74dbe19c92f5052774eadb15136263548dd013250f1ed1027988e7fef125c8d"
],
"version": "==1.0.2"
},
"diff-match-patch": {
"hashes": [
"sha256:9dba5611fbf27893347349fd51cc1911cb403682a7163373adacc565d11e2e4c"
],
"index": "pypi",
"version": "==20121119"
},
"django": {
"hashes": [
"sha256:29268cc47816a44f27308e60f71da635f549c47d8a1d003b28de55141df75791",
"sha256:37f5876c1fbfd66085001f4c06fa0bf96ef05442c53daf8d4294b6f29e7fa6b8"
],
"index": "pypi",
"version": "==1.11.16"
},
"django-ipware": {
"hashes": [
"sha256:93a90f9dd8caf2c633172aa8c8ba4e76e2b44f92a6942fa35e7624281e81ea03"
],
"index": "pypi",
"version": "==1.1.6"
},
"django-jinja": {
"hashes": [
"sha256:5e826a0cce967f40e6fdc037ea23667a2d3cd072807c4c87ffcc010b3c59121f",
"sha256:ebfde44cb716e57a9cdff6c1a4935fc49c7419ea4cd0b2b89bcecc696b9c0c86"
],
"index": "pypi",
"version": "==2.3.1"
},
"django-pglocks": {
"hashes": [
"sha256:9405ee54bbf157bb16b814f20ea7ad9d82d5cf26f9bf3ea8e3a71032179844cf"
],
"index": "pypi",
"version": "==1.0.2"
},
"django-picklefield": {
"hashes": [
"sha256:5489fef164de43725242d56e65e016137d3df0d1a00672bda72d807f5b2b0d99",
"sha256:fab48a427c6310740755b242128f9300283bef159ffee42d3231a274c65d9ae2"
],
"index": "pypi",
"version": "==0.3.2"
},
"django-sampledatahelper": {
"hashes": [
"sha256:96d0a599054979eb9669d44a1735236da42d56be0c45d4bcd34c2a3acefb259d"
],
"index": "pypi",
"version": "==0.4.1"
},
"django-sites": {
"hashes": [
"sha256:fa1470bd72be589f3891b7cd28dd2d782d8b34539665d9334e49154566f3d916"
],
"index": "pypi",
"version": "==0.9"
},
"django-sr": {
"hashes": [
"sha256:3586b852ae8af1b4b2796590534b0b867b523f47a5779b2ccb6ce010efc57e34"
],
"index": "pypi",
"version": "==0.0.4"
},
"djmail": {
"hashes": [
"sha256:370f57230bf79004840ddd1cb8150a5b7676b2aa14bc5b62027106cb708a47a0"
],
"index": "pypi",
"version": "==1.0.1"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"easy-thumbnails": {
"hashes": [
"sha256:5cc51c6ec7de110355d0f8cd56c9ede6e2949e87c2fcb34bc864a20ecd424270",
"sha256:6e41e70a182a6d00af9f3f3a6d1cc87cb7da060a3d56982da51d266e40fc9b59"
],
"index": "pypi",
"version": "==2.4.1"
},
"fn": {
"hashes": [
"sha256:f8cd80cdabf15367a2f07e7a9951fdc013d7200412743d85b88f2c896c95bada"
],
"index": "pypi",
"version": "==0.4.3"
},
"gunicorn": {
"hashes": [
"sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
"sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
],
"index": "pypi",
"version": "==19.7.1"
},
"html5lib": {
"hashes": [
"sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3",
"sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"
],
"version": "==1.0.1"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"jinja2": {
"hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.10"
},
"kombu": {
"hashes": [
"sha256:385bf38e6de7f3851f674671dbfe24572ce999608d293a85fb8a630654d8bd9c",
"sha256:d0fc6f2a36610a308f838db4b832dad79a681b516ac1d1a1f9d42edb58cc11a2"
],
"index": "pypi",
"version": "==4.0.2"
},
"lxml": {
"hashes": [
"sha256:02bc220d61f46e9b9d5a53c361ef95e9f5e1d27171cd461dddb17677ae2289a5",
"sha256:22f253b542a342755f6cfc047fe4d3a296515cf9b542bc6e261af45a80b8caf6",
"sha256:2f31145c7ff665b330919bfa44aacd3a0211a76ca7e7b441039d2a0b0451e415",
"sha256:36720698c29e7a9626a0dc802ef8885f8f0239bfd1689628ecd459a061f2807f",
"sha256:438a1b0203545521f6616132bfe0f4bca86f8a401364008b30e2b26ec408ce85",
"sha256:4815892904c336bbaf73dafd54f45f69f4021c22b5bad7332176bbf4fb830568",
"sha256:5be031b0f15ad63910d8e5038b489d95a79929513b3634ad4babf77100602588",
"sha256:5c93ae37c3c588e829b037fdfbd64a6e40c901d3f93f7beed6d724c44829a3ad",
"sha256:60842230678674cdac4a1cf0f707ef12d75b9a4fc4a565add4f710b5fcf185d5",
"sha256:62939a8bb6758d1bf923aa1c13f0bcfa9bf5b2fc0f5fa917a6e25db5fe0cfa4e",
"sha256:75830c06a62fe7b8fe3bbb5f269f0b308f19f3949ac81cfd40062f47c1455faf",
"sha256:81992565b74332c7c1aff6a913a3e906771aa81c9d0c68c68113cffcae45bc53",
"sha256:8c892fb0ee52c594d9a7751c7d7356056a9682674b92cc1c4dc968ff0f30c52f",
"sha256:9d862e3cf4fc1f2837dedce9c42269c8c76d027e49820a548ac89fdcee1e361f",
"sha256:a623965c086a6e91bb703d4da62dabe59fe88888e82c4117d544e11fd74835d6",
"sha256:a7783ab7f6a508b0510490cef9f857b763d796ba7476d9703f89722928d1e113",
"sha256:aab09fbe8abfa3b9ce62aaf45aca2d28726b1b9ee44871dbe644050a2fff4940",
"sha256:abf181934ac3ef193832fb973fd7f6149b5c531903c2ec0f1220941d73eee601",
"sha256:ae07fa0c115733fce1e9da96a3ac3fa24801742ca17e917e0c79d63a01eeb843",
"sha256:b9c78242219f674ab645ec571c9a95d70f381319a23911941cd2358a8e0521cf",
"sha256:bccb267678b870d9782c3b44d0cefe3ba0e329f9af8c946d32bf3778e7a4f271",
"sha256:c4df4d27f4c93b2cef74579f00b1d3a31a929c7d8023f870c4b476f03a274db4",
"sha256:caf0e50b546bb60dfa99bb18dfa6748458a83131ecdceaf5c071d74907e7e78a",
"sha256:d3266bd3ac59ac4edcd5fa75165dee80b94a3e5c91049df5f7c057ccf097551c",
"sha256:db0d213987bcd4e6d41710fb4532b22315b0d8fb439ff901782234456556aed1",
"sha256:dbbd5cf7690a40a9f0a9325ab480d0fccf46d16b378eefc08e195d84299bfae1",
"sha256:e16e07a0ec3a75b5ee61f2b1003c35696738f937dc8148fbda9fe2147ccb6e61",
"sha256:e175a006725c7faadbe69e791877d09936c0ef2cf49d01b60a6c1efcb0e8be6f",
"sha256:edd9c13a97f6550f9da2236126bb51c092b3b1ce6187f2bd966533ad794bbb5e",
"sha256:fa39ea60d527fbdd94215b5e5552f1c6a912624521093f1384a491a8ad89ad8b"
],
"version": "==4.2.5"
},
"markdown": {
"hashes": [
"sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa",
"sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
],
"index": "pypi",
"version": "==3.0.1"
},
"markupsafe": {
"hashes": [
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
],
"version": "==1.1.0"
},
"netaddr": {
"hashes": [
"sha256:38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd",
"sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca"
],
"index": "pypi",
"version": "==0.7.19"
},
"oauthlib": {
"hashes": [
"sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162",
"sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b"
],
"version": "==2.1.0"
},
"olefile": {
"hashes": [
"sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"
],
"version": "==0.46"
},
"pillow": {
"hashes": [
"sha256:00b6a5f28d00f720235a937ebc2f50f4292a5c7e2d6ab9a8b26153b625c4f431",
"sha256:025208f835383f425e93d574842f9c5d28918cd4cdf632c1ce2e72ab80d8fcc8",
"sha256:059a9b4e064b70e1396a3ae64781a91512f773cae548c24b12014616f723f22d",
"sha256:17f7702f22729ffeb69f5226abf3261ef2d2eb73ab5854c1294daa3bdb5bdfb7",
"sha256:20a3549d7a83e969eb900c726d54b34673efc1d0e3c9856b8227350f7e21d968",
"sha256:24258e1875c8a9de1b176bf1873436397669440d1561b06b00eb270bffefcb42",
"sha256:318b4404c8ca34cc1514d60de81ac4a0b0d11031a70341c2b7cd4fc01c914d89",
"sha256:33a71986741227c8c085ee5929171cd4c9376b2eee189cdc7acc7d81e1a3ca84",
"sha256:3499deb97561d6cc75de725fcf744491dd2d10d6213a29c4d62e3980e1522715",
"sha256:3ad24690882b68599b9e6b25309000881eebcc731ca499f8dcc549fab006e4a3",
"sha256:3c05df947656d8538dfb39fb8ddb9fe3594c9345911aa19f07e2ed0a8d148a6d",
"sha256:48cdae5e5291d355fc215ede2ea93738e243c2467b11e41fa5010a76fd278fc5",
"sha256:4b382c0ee6ac822673e1a57c2e9878d2ac4cd52038be097bac8535a2ee60ec0e",
"sha256:6458293cf299f02f17f58a1ee4b91f77b8ce7a38bc0e757838767f1389479953",
"sha256:8ef6627adfe9314b4132d4f5207563ba147e3977019ab1ca3f0b11b04c83c84f",
"sha256:9c508bf0b2aadad4349f69aebe080977dbf0cd055cefc15793c4165851a96933",
"sha256:9d7c0706cd86fc17643d78674cdac7f05590a2da1d71c42c2ebfb27df3889f17",
"sha256:a2a873b54881c4cf4d8b37f3c426c2b8f797b341e3893650763a62252bda3922",
"sha256:b79fc81352a3c907a1223499d790a4a7b77be342b794e19628046c4c95676356",
"sha256:c040a047209edaf860ce6dd5b55de718e047144b26b0ee4198dd19907c128eac",
"sha256:d71992826cfed66f5ad68364b2c6c3c1ab305294a642deae96ad77004981fb0f",
"sha256:e467d0977997b43ca80b7af42de3d1cfa779988f6507965e6d4fb1a004e963a0",
"sha256:ecf810b5019ce62846a9203bab82eb02bb9ed60e258b2fd89e2ca19a7010da46",
"sha256:f4fb801bfd2bcbfc4a7f2819c95ea6a1cfef197420ae9849b01b08b9970a51b3",
"sha256:f63404731fa5fa0c21d00af119b867e30208e3fc148c9b13fb6a541a8df203b2",
"sha256:f8e8f3f20e32f73f81ec408061f7a81ada07d7b3fac0787bdd233b93e4ff7d9c",
"sha256:fb3eaba16b6cf01f12860edccac40f98362bc17225575f3bcabb333d0b4ed6dc"
],
"index": "pypi",
"version": "==4.1.1"
},
"premailer": {
"hashes": [
"sha256:25c97c4c1838a8045ed1da1a14bd82ce458687878aa162378a78aa82a6aec6af",
"sha256:4e71cc09ad1438f827d1070ffac54ceb3a6a07c995fa82cb34c1ef163adeb432"
],
"index": "pypi",
"version": "==3.0.1"
},
"psd-tools": {
"hashes": [
"sha256:1a8dd69783c84217f3a25938916c289e5653543c5610e9013769520e8bd65b3c",
"sha256:7f3fd8577fb627405a204a65d4ccea48299f95338a71fbd9709bdb9d7789cc4e",
"sha256:f519a08049c19b3ef37ccc2b39af11ac5ded59d2674fc4c28103e4db57a9aa99"
],
"index": "pypi",
"version": "==1.4"
},
"psycopg2-binary": {
"hashes": [
"sha256:04afb59bbbd2eab3148e6816beddc74348078b8c02a1113ea7f7822f5be4afe3",
"sha256:098b18f4d8857a8f9b206d1dc54db56c2255d5d26458917e7bcad61ebfe4338f",
"sha256:0bf855d4a7083e20ead961fda4923887094eaeace0ab2d76eb4aa300f4bbf5bd",
"sha256:197dda3ffd02057820be83fe4d84529ea70bf39a9a4daee1d20ffc74eb3d042e",
"sha256:278ef63afb4b3d842b4609f2c05ffbfb76795cf6a184deeb8707cd5ed3c981a5",
"sha256:3cbf8c4fc8f22f0817220891cf405831559f4d4c12c4f73913730a2ea6c47a47",
"sha256:4305aed922c4d9d6163ab3a41d80b5a1cfab54917467da8168552c42cad84d32",
"sha256:47ee296f704fb8b2a616dec691cdcfd5fa0f11943955e88faa98cbd1dc3b3e3d",
"sha256:4a0e38cb30457e70580903367161173d4a7d1381eb2f2cfe4e69b7806623f484",
"sha256:4d6c294c6638a71cafb82a37f182f24321f1163b08b5d5ca076e11fe838a3086",
"sha256:4f3233c366500730f839f92833194fd8f9a5c4529c8cd8040aa162c3740de8e5",
"sha256:5221f5a3f4ca2ddf0d58e8b8a32ca50948be9a43351fda797eb4e72d7a7aa34d",
"sha256:5c6ca0b507540a11eaf9e77dee4f07c131c2ec80ca0cffa146671bf690bc1c02",
"sha256:789bd89d71d704db2b3d5e67d6d518b158985d791d3b2dec5ab85457cfc9677b",
"sha256:7b94d29239efeaa6a967f3b5971bd0518d2a24edd1511edbf4a2c8b815220d07",
"sha256:89bc65ef3301c74cf32db25334421ea6adbe8f65601ea45dcaaf095abed910bb",
"sha256:89d6d3a549f405c20c9ae4dc94d7ed2de2fa77427a470674490a622070732e62",
"sha256:97521704ac7127d7d8ba22877da3c7bf4a40366587d238ec679ff38e33177498",
"sha256:a395b62d5f44ff6f633231abe568e2203b8fabf9797cd6386aa92497df912d9a",
"sha256:a6d32c37f714c3f34158f3fa659f3a8f2658d5f53c4297d45579b9677cc4d852",
"sha256:a89ee5c26f72f2d0d74b991ce49e42ddeb4ac0dc2d8c06a0f2770a1ab48f4fe0",
"sha256:b4c8b0ef3608e59317bfc501df84a61e48b5445d45f24d0391a24802de5f2d84",
"sha256:b5fcf07140219a1f71e18486b8dc28e2e1b76a441c19374805c617aa6d9a9d55",
"sha256:b86f527f00956ecebad6ab3bb30e3a75fedf1160a8716978dd8ce7adddedd86f",
"sha256:be4c4aa22ba22f70de36c98b06480e2f1697972d49eb20d525f400d204a6d272",
"sha256:c2ac7aa1a144d4e0e613ac7286dae85671e99fe7a1353954d4905629c36b811c",
"sha256:de26ef4787b5e778e8223913a3e50368b44e7480f83c76df1f51d23bd21cea16",
"sha256:e70ebcfc5372dc7b699c0110454fc4263967f30c55454397e5769eb72c0eb0ce",
"sha256:eadbd32b6bc48b67b0457fccc94c86f7ccc8178ab839f684eb285bb592dc143e",
"sha256:ecbc6dfff6db06b8b72ae8a2f25ff20fbdcb83cb543811a08f7cb555042aa729"
],
"index": "pypi",
"version": "==2.7.5"
},
"pycparser": {
"hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
],
"version": "==2.19"
},
"pygments": {
"hashes": [
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
],
"index": "pypi",
"version": "==2.2.0"
},
"pyjwt": {
"hashes": [
"sha256:00414bfef802aaecd8cc0d5258b6cb87bd8f553c2986c2c5f29b19dd5633aeb7",
"sha256:ddec8409c57e9d371c6006e388f91daf3b0b43bdf9fcbf99451fb7cf5ce0a86d"
],
"version": "==1.7.0"
},
"python-dateutil": {
"hashes": [
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
],
"index": "pypi",
"version": "==2.7.5"
},
"python-magic": {
"hashes": [
"sha256:604eace6f665809bebbb07070508dfa8cabb2d7cb05be9a56706c60f864f1289",
"sha256:a1ec69e76cc513b1af164c02982607f96ff3bb668162a688f2b1bb5f6a5fe05d"
],
"index": "pypi",
"version": "==0.4.13"
},
"pytz": {
"hashes": [
"sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
"sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
],
"index": "pypi",
"version": "==2018.7"
},
"raven": {
"hashes": [
"sha256:02cabffb173b99d860a95d4908e8b1864aad1b8452146e13fd7e212aa576a884",
"sha256:56dc9062dd42bca97350e5048ff417c914376366caa3b1b5f788b27ddc0a34b7"
],
"index": "pypi",
"version": "==6.1.0"
},
"redis": {
"hashes": [
"sha256:5dfbae6acfc54edf0a7a415b99e0b21c0a3c27a7f787b292eea727b1facc5533",
"sha256:97156b37d7cda4e7d8658be1148c983984e1a975090ba458cc7e244025191dbd"
],
"index": "pypi",
"version": "==2.10.5"
},
"requests": {
"hashes": [
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"index": "pypi",
"version": "==2.20.0"
},
"requests-oauthlib": {
"hashes": [
"sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca",
"sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468"
],
"version": "==0.8.0"
},
"sampledata": {
"hashes": [
"sha256:b06916ef010d3c1a9db8aa314a144f24ad421f28597ff8c568603c451391a2cf"
],
"version": "==0.3.7"
},
"serpy": {
"hashes": [
"sha256:b1481f8cb93d767b23903d1df6cc0a7120cb0694095b6695eb78d9d453b23c65",
"sha256:b774bfdf0c3b245660639e9fc5f311a8bbceb2725aaba72fce1fec00b453286e"
],
"index": "pypi",
"version": "==0.1.1"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"tinycss": {
"hashes": [
"sha256:12306fb50e5e9e7eaeef84b802ed877488ba80e35c672867f548c0924a76716e"
],
"version": "==0.4"
},
"unidecode": {
"hashes": [
"sha256:ed4418b4b1b190487753f1cca6299e8076079258647284414e6d607d1f8a00e0",
"sha256:eedac7bfd886f43484787206f6a141b232e2b2a58652c54d06499b187fd84660"
],
"index": "pypi",
"version": "==0.4.20"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
},
"vine": {
"hashes": [
"sha256:52116d59bc45392af9fdd3b75ed98ae48a93e822cee21e5fda249105c59a7a72",
"sha256:6849544be74ec3638e84d90bc1cf2e1e9224cc10d96cd4383ec3f69e9bce077b"
],
"version": "==1.1.4"
},
"webcolors": {
"hashes": [
"sha256:e47e68644d41c0b1f1e4d939cfe4039bdf1ab31234df63c7a4f59d4766487206"
],
"index": "pypi",
"version": "==1.7"
},
"webencodings": {
"hashes": [
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
],
"version": "==0.5.1"
}
},
"develop": {
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
],
"version": "==1.2.1"
},
"attrs": {
"hashes": [
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
],
"version": "==18.2.0"
},
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"coverage": {
"hashes": [
"sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f",
"sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe",
"sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d",
"sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0",
"sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607",
"sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d",
"sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b",
"sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3",
"sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e",
"sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815",
"sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36",
"sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1",
"sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14",
"sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c",
"sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794",
"sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b",
"sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840",
"sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd",
"sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82",
"sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952",
"sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389",
"sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f",
"sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4",
"sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da",
"sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647",
"sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d",
"sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42",
"sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478",
"sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b",
"sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb",
"sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"
],
"index": "pypi",
"version": "==4.5.2"
},
"coveralls": {
"hashes": [
"sha256:ab638e88d38916a6cedbf80a9cd8992d5fa55c77ab755e262e00b36792b7cd6d",
"sha256:b2388747e2529fa4c669fb1e3e2756e4e07b6ee56c7d9fce05f35ccccc913aa0"
],
"index": "pypi",
"version": "==1.5.1"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"factory-boy": {
"hashes": [
"sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca",
"sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f"
],
"index": "pypi",
"version": "==2.11.1"
},
"faker": {
"hashes": [
"sha256:c61a41d0dab8865b850bd00454fb11e90f3fd2a092d8bc90120d1e1c01cff906",
"sha256:f909ff9133ce0625ca388b6838190630ad7a593f87eaf058d872338a76241d5d"
],
"version": "==1.0.0"
},
"idna": {
"hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.7"
},
"more-itertools": {
"hashes": [
"sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
"sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
"sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
],
"version": "==4.3.0"
},
"pluggy": {
"hashes": [
"sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
"sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
],
"version": "==0.8.0"
},
"py": {
"hashes": [
"sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
"sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
],
"version": "==1.7.0"
},
"pytest": {
"hashes": [
"sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec",
"sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"
],
"index": "pypi",
"version": "==3.10.1"
},
"pytest-django": {
"hashes": [
"sha256:49e9ffc856bc6a1bec1c26c5c7b7213dff7cc8bc6b64d624c4d143d04aff0bcf",
"sha256:b379282feaf89069cb790775ab6bbbd2bd2038a68c7ef9b84a41898e0b551081"
],
"index": "pypi",
"version": "==3.4.3"
},
"python-dateutil": {
"hashes": [
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
],
"index": "pypi",
"version": "==2.7.5"
},
"requests": {
"hashes": [
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"index": "pypi",
"version": "==2.20.0"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"text-unidecode": {
"hashes": [
"sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d",
"sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"
],
"version": "==1.2"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
}
}
}

View File

@ -96,7 +96,7 @@ python manage.py loaddata initial_project_templates
python manage.py sample_data
```
**IMPORTANT: Taiga only runs with python 3.4+**
**IMPORTANT: Taiga only runs with python 3.5+**
Initial auth data: admin/123123

View File

@ -1,3 +1,6 @@
[pytest]
DJANGO_SETTINGS_MODULE = settings.testing
python_paths = .
filterwarnings =
once
ignore::django.utils.deprecation.RemovedInDjango20Warning
ignore::DeprecationWarning:taiga.base.api.serializers

View File

@ -1,11 +0,0 @@
-r requirements.txt
coverage==4.4.1
coveralls==1.1
django-slowdown==0.0.1
factory_boy==2.8.1
py==1.4.34
pytest-django==3.1.2
pytest-pythonpath==0.7.1
pytest==3.1.1
transifex-client==0.12.4

View File

@ -1,45 +1,63 @@
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
CairoSVG==2.0.3
Django==1.11.2
git+git://github.com/Python-Markdown/markdown.git@a4d4b61b5ce4a7dd96ddc13c66b8716d3ad8eb51 # This version solves 'infinite execution' and other issues.
Pillow==4.1.1
PyJWT==1.5.0
Unidecode==0.4.20
amqp==2.1.4
-i https://pypi.org/simple
amqp==2.3.2
asana==0.6.7
bleach==2.0.0
asn1crypto==0.24.0
billiard==3.5.0.5
bleach==2.1.4
cairocffi==0.9.0
cairosvg==2.0.3
celery==4.0.2
kombu==4.0.2
cryptography==1.9
certifi==2018.11.29
cffi==1.11.5
chardet==3.0.4
contextlib2==0.5.5
cryptography==2.3.1
cssselect==1.0.3
cssutils==1.0.2
diff-match-patch==20121119
django-ipware==1.1.6
django-jinja==2.3.1
django-pglocks==1.0.2
django-picklefield==0.3.2
django-sampledatahelper==0.4.1
django-sites==0.9
django-sr==0.0.4
django==1.11.16
djmail==1.0.1
docopt==0.6.2
easy-thumbnails==2.4.1
fn==0.4.3
git+https://github.com/Xof/django-pglocks.git
gunicorn==19.7.1
jinja2==2.9.6
idna==2.5
lxml==3.8.0
html5lib==1.0.1
idna==2.7
jinja2==2.10
kombu==4.0.2
lxml==4.2.5
markdown==3.0.1
markupsafe==1.1.0
netaddr==0.7.19
oauthlib==2.1.0
olefile==0.46
pillow==4.1.1
premailer==3.0.1
psd-tools==1.4
psycopg2==2.7.4
psycopg2-binary==2.7.5
pycparser==2.19
pygments==2.2.0
pyjwkest==1.3.2
python-dateutil==2.6.0
pyjwt==1.7.0
python-dateutil==2.7.5
python-magic==0.4.13
pytz==2017.2
pytz==2018.7
raven==6.1.0
redis==2.10.5
requests-oauthlib==0.8.0
requests==2.17.3
requests==2.20.0
sampledata==0.3.7
serpy==0.1.1
six==1.10.0
six==1.11.0
tinycss==0.4
unidecode==0.4.20
urllib3==1.24.1
vine==1.1.4
webcolors==1.7
webencodings==0.5.1

View File

@ -242,11 +242,7 @@ You need transifex-client, install it.
1. Install transifex-client, use
$ pip install --upgrade -r requirements-devel.txt
or
$ pip install --upgrade transifex-client==0.12.2
$ pip install --upgrade transifex-client
2. Create ~/.transifexrc file:

View File

@ -1,4 +0,0 @@
from .celery import *
# To use celery in memory
#task_always_eager = True

View File

@ -148,7 +148,7 @@ LANGUAGES = [
("tr", "Türkçe"), # Turkish
#("tt", "татар теле"), # Tatar
#("udm", "удмурт кыл"), # Udmurt
#("uk", "Українська"), # Ukrainian
("uk", "Українська"), # Ukrainian
#("ur", "اردو‏"), # Urdu
#("vi", "Tiếng Việt"), # Vietnamese
("zh-hans", "中文(简体)"), # Simplified Chinese
@ -309,6 +309,7 @@ INSTALLED_APPS = [
"taiga.projects.issues",
"taiga.projects.wiki",
"taiga.projects.contact",
"taiga.projects.settings",
"taiga.searches",
"taiga.timeline",
"taiga.mdrender",
@ -547,6 +548,7 @@ EXPORTS_TTL = 60 * 60 * 24 # 24 hours
CELERY_ENABLED = False
WEBHOOKS_ENABLED = False
WEBHOOKS_BLOCK_PRIVATE_ADDRESS = False
# If is True /front/sitemap.xml show a valid sitemap of taiga-front client

View File

@ -2,7 +2,10 @@
ignore = E41,E266
max-line-length = 120
exclude =
.cache,
.git,
.tox,
.venv,
*__pycache__*,
*tests*,
*scripts*,

View File

@ -375,14 +375,18 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi
class BaseRelatedFieldsFilter(FilterBackend):
filter_name = None
param_name = None
exclude_param_name = None
def __init__(self, filter_name=None, param_name=None):
def __init__(self, filter_name=None, param_name=None, exclude_param_name=None):
if filter_name:
self.filter_name = filter_name
if param_name:
self.param_name = param_name
if exclude_param_name:
self.exclude_param_name
def _prepare_filter_data(self, query_param_value):
def _transform_value(value):
try:
@ -396,48 +400,57 @@ class BaseRelatedFieldsFilter(FilterBackend):
values = map(_transform_value, values)
return list(values)
def _get_queryparams(self, params):
param_name = self.param_name or self.filter_name
def _get_queryparams(self, params, mode=''):
param_name = self.exclude_param_name if mode == 'exclude' else self.param_name or self.filter_name
raw_value = params.get(param_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 Q(**{"{}__in".format(self.filter_name): value})
return None
def _prepare_filter_query(self, query):
return query
def _prepare_exclude_query(self, query):
return ~Q(query)
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)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, prepare_method in operations.items():
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
queryset = queryset.filter(prepare_method(query))
return super().filter_queryset(request, queryset, view)
class OwnersFilter(BaseRelatedFieldsFilter):
filter_name = 'owner'
exclude_param_name = 'exclude_owner'
class AssignedToFilter(BaseRelatedFieldsFilter):
filter_name = 'assigned_to'
exclude_param_name = 'exclude_assigned_to'
class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
filter_name = 'assigned_users'
exclude_param_name = 'exclude_assigned_users'
def _get_queryparams(self, params):
param_name = self.param_name or self.filter_name
def _get_queryparams(self, params, mode=''):
param_name = self.exclude_param_name if mode == 'exclude' else self.param_name or self.filter_name
raw_value = params.get(param_name, None)
if raw_value:
value = self._prepare_filter_data(raw_value)
UserStoryModel = apps.get_model("userstories", "UserStory")
@ -461,38 +474,65 @@ class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
class StatusesFilter(BaseRelatedFieldsFilter):
filter_name = 'status'
exclude_param_name = 'exclude_status'
class IssueTypesFilter(BaseRelatedFieldsFilter):
filter_name = 'type'
param_name = 'type'
exclude_param_name = 'exclude_type'
class PrioritiesFilter(BaseRelatedFieldsFilter):
filter_name = 'priority'
exclude_param_name = 'exclude_priority'
class SeveritiesFilter(BaseRelatedFieldsFilter):
filter_name = 'severity'
exclude_param_name = 'exclude_severity'
class TagsFilter(FilterBackend):
filter_name = 'tags'
exclude_param_name = 'exclude_tags'
def __init__(self, filter_name=None):
def __init__(self, filter_name=None, exclude_param_name=None):
if filter_name:
self.filter_name = filter_name
def _get_tags_queryparams(self, params):
tags = params.get(self.filter_name, None)
if exclude_param_name:
self.exclude_param_name = exclude_param_name
def _get_tags_queryparams(self, params, mode=''):
param_name = self.exclude_param_name if mode == "exclude" else self.filter_name
tags = params.get(param_name, None)
if tags:
return tags.split(",")
return None
def _prepare_filter_query(self, query):
return Q(tags__contains=query)
def _prepare_exclude_query(self, tags):
queries = [Q(tags__contains=[tag]) for tag in tags]
query = queries.pop()
for item in queries:
query |= item
return ~Q(query)
def filter_queryset(self, request, queryset, view):
query_tags = self._get_tags_queryparams(request.QUERY_PARAMS)
if query_tags:
queryset = queryset.filter(tags__contains=query_tags)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, prepare_method in operations.items():
query = self._get_tags_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
queryset = queryset.filter(prepare_method(query))
return super().filter_queryset(request, queryset, view)
@ -631,18 +671,22 @@ class QFilter(FilterBackend):
class RoleFilter(BaseRelatedFieldsFilter):
filter_name = "role_id"
param_name = "role"
exclude_param_name = "exclude_role"
def filter_queryset(self, request, queryset, view):
Membership = apps.get_model('projects', 'Membership')
query = self._get_queryparams(request.QUERY_PARAMS)
if query:
if isinstance(query, dict):
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
queryset = queryset.filter(assigned_to__in=memberships)
else:
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
if memberships:
queryset = queryset.filter(assigned_to__in=memberships)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, qs_method in operations.items():
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
memberships = Membership.objects.filter(query).exclude(user__isnull=True).values_list("user_id", flat=True)
if memberships:
queryset = queryset.filter(qs_method(Q(assigned_to__in=memberships)))
return FilterBackend.filter_queryset(self, request, queryset, view)
@ -650,20 +694,24 @@ class RoleFilter(BaseRelatedFieldsFilter):
class UserStoriesRoleFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
filter_name = "role_id"
param_name = "role"
exclude_param_name = 'exclude_role'
def filter_queryset(self, request, queryset, view):
Membership = apps.get_model('projects', 'Membership')
query = self._get_queryparams(request.QUERY_PARAMS)
if query:
if isinstance(query, dict):
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
else:
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
if memberships:
user_story_model = apps.get_model("userstories", "UserStory")
queryset = queryset.filter(
self.get_assigned_users_filter(user_story_model, memberships)
)
operations = {
"filter": self._prepare_filter_query,
"exclude": self._prepare_exclude_query,
}
for mode, qs_method in operations.items():
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
if query:
memberships = Membership.objects.filter(query).exclude(user__isnull=True).values_list("user_id", flat=True)
if memberships:
user_story_model = apps.get_model("userstories", "UserStory")
queryset = queryset.filter(
qs_method(Q(self.get_assigned_users_filter(user_story_model, memberships)))
)
return FilterBackend.filter_queryset(self, request, queryset, view)

View File

@ -68,7 +68,7 @@ def psd_image_factory(data, *args):
Image.register_open("PSD", psd_image_factory)
def get_thumbnail_url(file_obj, thumbnailer_size):
def get_thumbnail(file_obj, thumbnailer_size):
# Ugly hack to temporary ignore tiff files
relative_name = file_obj
if isinstance(file_obj, FieldFile):
@ -79,9 +79,18 @@ def get_thumbnail_url(file_obj, thumbnailer_size):
return None
try:
path_url = get_thumbnailer(file_obj)[thumbnailer_size].url
thumb_url = get_absolute_url(path_url)
thumbnailer = get_thumbnailer(file_obj)
return thumbnailer[thumbnailer_size]
except InvalidImageFormatError:
thumb_url = None
return None
def get_thumbnail_url(file_obj, thumbnailer_size):
thumbnail = get_thumbnail(file_obj, thumbnailer_size)
if not thumbnail:
return None
path_url = thumbnail.url
thumb_url = get_absolute_url(path_url)
return thumb_url

View File

@ -17,8 +17,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import ipaddress
import socket
from urllib.parse import urlparse
import django_sites as sites
from django.core.urlresolvers import reverse as django_reverse
from django.utils.translation import ugettext as _
URL_TEMPLATE = "{scheme}://{domain}/{path}"
@ -43,3 +48,29 @@ def get_absolute_url(path):
def reverse(viewname, *args, **kwargs):
"""Same behavior as django's reverse but uses django_sites to compute absolute url."""
return get_absolute_url(django_reverse(viewname, *args, **kwargs))
class HostnameException(Exception):
pass
class IpAddresValueError(ValueError):
pass
def validate_private_url(url):
host = urlparse(url).hostname
port = urlparse(url).port
try:
socket_args, *others = socket.getaddrinfo(host, port)
except Exception:
raise HostnameException(_("Host access error"))
destination_address = socket_args[4][0]
try:
ipa = ipaddress.ip_address(destination_address)
except ValueError:
raise IpAddresValueError(_("IP Address error"))
if ipa.is_private:
raise IpAddresValueError("Private IP Address not allowed")

View File

@ -91,6 +91,22 @@ def emit_event_for_model(obj, *, type:str="change", channel:str="events",
sessionid=sessionid,
data=data)
def emit_event_for_user_notification(user_id,
*,
session_id: str=None,
event_type: str=None,
data: dict=None):
"""
Sends a user notification event.
"""
return emit_event(
data,
"web_notifications.{}".format(user_id),
sessionid=session_id
)
def emit_live_notification_for_model(obj, user, history, *, type:str="change", channel:str="events",
sessionid:str="not-existing"):
"""

View File

@ -19,6 +19,8 @@
from taiga.users.models import User
from taiga.projects.models import Membership
from taiga.permissions.choices import ANON_PERMISSIONS
def resolve_users_bindings(users_bindings):
new_users_bindings = {}
@ -50,3 +52,15 @@ def create_memberships(users_bindings, project, creator, role_name):
is_admin=False,
invited_by=creator,
)
def set_base_permissions_for_project(project):
if project.is_private:
return
anon_permissions = list(
map(lambda perm: perm[0], ANON_PERMISSIONS))
project.anon_permissions = list(
set((project.anon_permissions or []) + anon_permissions))
project.public_permissions = list(
set((project.public_permissions or []) + anon_permissions))
project.save()

View File

@ -262,6 +262,7 @@ class TrelloImporter:
project=project
)
import_service.create_memberships(options.get('users_bindings', {}), project, self._user, "trello")
import_service.set_base_permissions_for_project(project)
return project
def _import_user_stories_data(self, data, project, options):

View File

@ -10,8 +10,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@ -196,8 +196,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
@ -480,6 +480,14 @@ msgstr ""
" Comentari: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -983,7 +991,7 @@ msgid "The payload is not a valid json"
msgstr "El payload no és un arxiu json vàlid"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "El projecte no existeix"
@ -3110,15 +3118,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr ""
@ -4276,3 +4284,7 @@ msgstr ""
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr ""
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -26,9 +26,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-09-04 08:41+0000\n"
"Last-Translator: Jonas Zürcher <info@jonaszuercher.ch>\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-11-14 12:59+0000\n"
"Last-Translator: Hans Raaf\n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
"MIME-Version: 1.0\n"
@ -234,8 +234,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Blockiertes Element"
@ -547,9 +547,17 @@ msgstr ""
"Kommentar: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr "User story erstellt"
msgstr "User-Story erstellt"
#: taiga/events/events.py:109
msgid "User story changed"
@ -569,15 +577,15 @@ msgstr "Aufgabe erzeugt"
#: taiga/events/events.py:120
msgid "Task changed"
msgstr "Task geändert"
msgstr "Aufgabe geändert"
#: taiga/events/events.py:123
msgid "Task deleted"
msgstr "Task gelöscht"
msgstr "Aufgabe gelöscht"
#: taiga/events/events.py:125
msgid "Task #{} - {}"
msgstr "Task #{} - {}"
msgstr "Aufgabe #{} - {}"
#: taiga/events/events.py:128
msgid "Issue created"
@ -1158,7 +1166,7 @@ msgid "The payload is not a valid json"
msgstr "Die Nutzlast ist kein gültiges json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Das Projekt existiert nicht"
@ -2833,8 +2841,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Neues Ticket wurde erstellt</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat ein neues Ticket erstellt in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat ein neues Ticket in "
"%(project)s erstellt</p>\n"
"<p>Ticket #%(ref)s %(subject)s</p>\n"
"<a class=\"button\" href=\"%(url)s\" title=\"See Issue #%(ref)s %(subject)s"
"\">Ticket ansehen</a>\n"
@ -2854,7 +2862,7 @@ msgid ""
msgstr ""
"\n"
"Neues Ticket wurde erstellt\n"
"Hallo %(user)s, %(changer)s hat ein neues Ticket erstellt in %(project)s\n"
"Hallo %(user)s, %(changer)s hat ein neues Ticket in %(project)s erstellt\n"
"Ticket ansehen #%(ref)s %(subject)s auf %(url)s\n"
"\n"
"---\n"
@ -2980,8 +2988,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Neuer Sprint wurde erstellt</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat einen neuen Sprint erstellt in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat einen neuen Sprint in "
"%(project)s erstellt</p>\n"
"<p>Sprint %(name)s</p>\n"
"<a class=\"button\" href=\"%(url)s\" title=\"See Sprint %(name)s\">See "
"sprint</a>\n"
@ -3001,7 +3009,7 @@ msgid ""
msgstr ""
"\n"
"Neuer Sprint wurde erstellt\n"
"Hallo %(user)s, %(changer)s hat einen neuen Sprint erstellt in %(project)s\n"
"Hallo %(user)s, %(changer)s hat einen neuen Sprint in %(project)s erstellt\n"
"Sprint ansehen %(name)s at %(url)s\n"
"\n"
"---\n"
@ -3050,7 +3058,7 @@ msgid ""
msgstr ""
"\n"
"Sprint wurde gelöscht\n"
"Hallo %(user)s, %(changer)s hat einen Sprint gelöscht in %(project)s\n"
"Hallo %(user)s, %(changer)s hat einen Sprint in %(project)s gelöscht \n"
"Sprint %(name)s\n"
"\n"
"---\n"
@ -3127,8 +3135,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Neue Aufgabe wurde erstellt</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Aufgabe erstellt in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Aufgabe in "
"%(project)s erstellt</p>\n"
"<p>Aufgabe #%(ref)s %(subject)s</p>\n"
"<a class=\"button\" href=\"%(url)s\" title=\"See Task #%(ref)s %(subject)s"
"\">Aufgabe ansehen</a>\n"
@ -3148,7 +3156,7 @@ msgid ""
msgstr ""
"\n"
"Neue Aufgabe wurde erstellt\n"
"Hallo %(user)s, %(changer)s hat eine neue Aufgabe erstellt in %(project)s\n"
"Hallo %(user)s, %(changer)s hat eine neue Aufgabe in %(project)s erstellt\n"
"Aufgabe ansehen #%(ref)s %(subject)s auf %(url)s\n"
"\n"
"\n"
@ -3179,8 +3187,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Aufgabe wurde gelöscht</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine Aufgabe gelöscht in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine Aufgabe in %(project)s "
"gelöscht</p>\n"
"<p>Aufgabe #%(ref)s %(subject)s</p>\n"
"<p><small>Das Taiga Team</small></p>\n"
" "
@ -3198,7 +3206,7 @@ msgid ""
msgstr ""
"\n"
"Aufgabe wurde gelöscht\n"
"Hallo %(user)s, %(changer)s hat eine Aufgabe gelöscht in %(project)s\n"
"Hallo %(user)s, %(changer)s hat eine Aufgabe in %(project)s gelöscht\n"
"Aufgabe #%(ref)s %(subject)s\n"
"\n"
"---\n"
@ -3275,8 +3283,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Neue User-Story wurde erstellt</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue User-Story erstellt in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue User-Story in "
"%(project)s erstellt</p>\n"
"<p>User-Story #%(ref)s %(subject)s</p>\n"
"<a class=\"button\" href=\"%(url)s\" title=\"See User Story #%(ref)s "
"%(subject)s\">User-Story ansehen</a>\n"
@ -3297,8 +3305,8 @@ msgid ""
msgstr ""
"\n"
"Neue User-Story wurde erstellt\n"
"Hallo %(user)s, %(changer)s hat eine neue User-Story erstellt in "
"%(project)s\n"
"Hallo %(user)s, %(changer)s hat eine neue User-Story in %(project)s "
"erstellt\n"
"User-Story ansehen #%(ref)s %(subject)s auf %(url)s\n"
"\n"
"---\n"
@ -3328,8 +3336,8 @@ msgid ""
msgstr ""
"\n"
"<h1>User-Story wurde gelöscht</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine User-Story gelöscht in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine User-Story in %(project)s "
"gelöscht</p>\n"
"<p>User-Story #%(ref)s %(subject)s</p>\n"
"<p><small>Das Taiga Team</small></p>\n"
"\n"
@ -3348,7 +3356,7 @@ msgid ""
msgstr ""
"\n"
"User-Story wurde gelöscht\n"
"Hallo %(user)s, %(changer)s hat eine User-Story gelöscht in %(project)s\n"
"Hallo %(user)s, %(changer)s hat eine User-Story in %(project)s gelöscht\n"
"User-Story #%(ref)s %(subject)s\n"
"\n"
"---\n"
@ -3430,8 +3438,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Neue Wiki Seite wurde erstellt</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Wiki Seite erstellt in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Wiki Seite in "
"%(project)s erstellt</p>\n"
"<p>Wiki Seite %(page)s</p>\n"
"<a class=\"button\" href=\"%(url)s\" title=\"Wiki page %(page)s\">Wiki Seite "
"ansehen</a>\n"
@ -3455,8 +3463,8 @@ msgstr ""
"\n"
"Neue Wiki Seite wurde erstellt\n"
"\n"
"Hallo %(user)s, %(changer)s hat eine neue Wiki Seite erstellt in "
"%(project)s\n"
"Hallo %(user)s, %(changer)s hat eine neue Wiki Seite in %(project)s "
"erstellt\n"
"\n"
"Wiki Seite ansehen %(page)s auf %(url)s\n"
"\n"
@ -3487,8 +3495,8 @@ msgid ""
msgstr ""
"\n"
"<h1>Wiki Seite wurde gelöscht</h1>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine Wiki Seite gelöscht in "
"%(project)s</p>\n"
"<p>Hallo %(user)s,<br />%(changer)s hat eine Wiki Seite in "
"%(project)s gelöscht</p>\n"
"<p>Wiki Seite %(page)s</p>\n"
"<p><small>Das Taiga Team</small></p>\n"
"\n"
@ -3510,7 +3518,7 @@ msgstr ""
"\n"
"Wiki Seite wurde gelöscht\n"
"\n"
"Hallo %(user)s, %(changer)s hat eine Wiki Seite gelöscht in %(project)s\n"
"Hallo %(user)s, %(changer)s hat eine Wiki Seite in %(project)s gelöscht\n"
"\n"
"Wiki Seite %(page)s\n"
"\n"
@ -3557,7 +3565,7 @@ msgstr ""
#: taiga/projects/services/members.py:133
msgid "Project without owner"
msgstr ""
msgstr "Projekt ohne Eigentümer"
#: taiga/projects/services/members.py:138
msgid "You have reached your current limit of memberships for private projects"
@ -3649,27 +3657,27 @@ msgstr "Dieser Tag existiert bereits"
#: taiga/projects/tagging/validators.py:54
#: taiga/projects/tagging/validators.py:81
msgid "The color is not a valid HEX color."
msgstr ""
msgstr "Diese Farbe ist keine gültige HEX Farbe."
#: taiga/projects/tagging/validators.py:67
#: taiga/projects/tagging/validators.py:101
#: taiga/projects/tagging/validators.py:114
#: taiga/projects/tagging/validators.py:121
msgid "The tag doesn't exist."
msgstr ""
msgstr "Dieses Tag existiert nicht."
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
"Sie haben nicht die Berechtigung, diesen Sprint auf diese Aufgabe zu setzen"
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
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:113
#: taiga/projects/tasks/api.py:114
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."
@ -3680,7 +3688,7 @@ msgstr "User-Story Befehl "
#: taiga/projects/tasks/models.py:61
msgid "taskboard order"
msgstr "Taskboard Befehl "
msgstr "Aufgabenlisten Sortierung "
#: taiga/projects/tasks/models.py:69
msgid "is iocaine"
@ -3692,7 +3700,7 @@ msgstr "Ungültige milestone id."
#: taiga/projects/tasks/validators.py:72
msgid "Invalid task status id."
msgstr "Ungültige task status id."
msgstr "Ungültige Aufgaben Status Id."
#: taiga/projects/tasks/validators.py:85
msgid "Invalid user story id."
@ -4965,3 +4973,7 @@ msgstr "Antwort Header"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "Dauer"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-02-15 17:25+0000\n"
"Last-Translator: Miguel Gonzalez <migonzalvar@gmail.com>\n"
"Language-Team: English (http://www.transifex.com/taiga-agile-llc/taiga-back/"
@ -196,8 +196,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Blocked element"
@ -518,6 +518,14 @@ msgstr ""
" Comment: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1123,7 +1131,7 @@ msgid "The payload is not a valid json"
msgstr "The payload is not a valid json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "The project doesn't exist"
@ -3667,15 +3675,15 @@ msgstr "The color is not a valid HEX color."
msgid "The tag doesn't exist."
msgstr "The tag doesn't exist."
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "You don't have permissions to set this sprint to this task."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "You don't have permissions to set this user story to this task."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "You don't have permissions to set this status to this task."
@ -5021,3 +5029,7 @@ msgstr "response headers"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duration"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -21,8 +21,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@ -217,8 +217,8 @@ msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada.
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Elemento bloqueado"
@ -536,6 +536,14 @@ msgstr ""
"\n"
"Comentario: %(comment)s"
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1141,7 +1149,7 @@ msgid "The payload is not a valid json"
msgstr "El payload no es un json válido"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "El proyecto no existe"
@ -3681,15 +3689,15 @@ msgstr "El color no tiene un código hexadecimal válido."
msgid "The tag doesn't exist."
msgstr "El tag no existe"
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
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:110
#: taiga/projects/tasks/api.py:111
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:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "No tienes permisos para asignar este estado a esta tarea."
@ -5043,3 +5051,7 @@ msgstr "cabeceras de la respuesta"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duración"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -4,14 +4,16 @@
#
# Translators:
# Translators:
# Amirhoshang Hoseinpour Dehkordi <amir.hoseinpour@gmail.com>, 2018
# Vahid Dayyani <vahid.dayyani@ymail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-12-02 10:54+0000\n"
"Last-Translator: Amirhoshang Hoseinpour Dehkordi <amir.hoseinpour@gmail."
"com>\n"
"Language-Team: Persian (Iran) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/fa_IR/)\n"
"MIME-Version: 1.0\n"
@ -26,7 +28,7 @@ msgstr "ثبت نام عمومی غیرفعال است."
#: taiga/auth/api.py:93
msgid "You must accept our terms of service and privacy policy"
msgstr ""
msgstr "شما باید موارد سرویس و سیاست های امنیت ما را قبول کنید."
#: taiga/auth/api.py:102
msgid "invalid register type"
@ -73,7 +75,7 @@ msgstr "نام کاربری نامعتبر"
#: taiga/auth/validators.py:42 taiga/users/validators.py:50
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr "ضروری است. ۲۵۵ کاراکتر یا کمتر. حروف و اعداد و . و - و ـ مجاز است."
msgstr "255 کاراکتر یا کمتر ضروری است. حروف و اعداد و . و - و ـ مجاز است."
#: taiga/base/api/fields.py:294
msgid "This field is required."
@ -86,7 +88,7 @@ msgstr "مقدار نامعتبر."
#: taiga/base/api/fields.py:484
#, python-format
msgid "'%s' value must be either True or False."
msgstr "'%s' می‌بایست True (صحیح) یا False (غلط) باشد."
msgstr "'%s' می‌بایست صحیح یا غلط باشد."
#: taiga/base/api/fields.py:549
msgid ""
@ -196,8 +198,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "المان مسدودشده"
@ -515,85 +517,93 @@ msgstr ""
" دیدگاه: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr "خطای دسترسی به هاست"
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr "خطای آدرس آی پی"
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
msgstr "گزارش کاربر ساخته شد."
#: taiga/events/events.py:109
msgid "User story changed"
msgstr ""
msgstr "گزارش کاربر تغییر کرد."
#: taiga/events/events.py:112
msgid "User story deleted"
msgstr ""
msgstr "گزارش کاربر حذف شد."
#: taiga/events/events.py:114
msgid "US #{} - {}"
msgstr ""
msgstr "US #{} - {}"
#: taiga/events/events.py:117
msgid "Task created"
msgstr ""
msgstr "وظیفه ساخته شد."
#: taiga/events/events.py:120
msgid "Task changed"
msgstr ""
msgstr "وظیفه تغییر کرد."
#: taiga/events/events.py:123
msgid "Task deleted"
msgstr ""
msgstr "وظیفه حذف شد."
#: taiga/events/events.py:125
msgid "Task #{} - {}"
msgstr ""
msgstr "وظیفه #{} - {}"
#: taiga/events/events.py:128
msgid "Issue created"
msgstr ""
msgstr "موضوع ساخته شد."
#: taiga/events/events.py:131
msgid "Issue changed"
msgstr ""
msgstr "موضوع تغییر کرد."
#: taiga/events/events.py:134
msgid "Issue deleted"
msgstr ""
msgstr "موضوع حذف شد."
#: taiga/events/events.py:136
msgid "Issue: #{} - {}"
msgstr ""
msgstr "موضوع: #{} - {}"
#: taiga/events/events.py:139
msgid "Wiki Page created"
msgstr ""
msgstr "صفحه ویکی ساخته شد."
#: taiga/events/events.py:142
msgid "Wiki Page changed"
msgstr ""
msgstr "صفحه ویکی تغییر داده شد."
#: taiga/events/events.py:145
msgid "Wiki Page deleted"
msgstr ""
msgstr "صفحه ویکی حذف شد."
#: taiga/events/events.py:147
msgid "Wiki Page: {}"
msgstr ""
msgstr "صفحه ویکی: {}"
#: taiga/events/events.py:150
msgid "Sprint created"
msgstr ""
msgstr "سرعتی ساخته شد."
#: taiga/events/events.py:153
msgid "Sprint changed"
msgstr ""
msgstr "سرعتی تغییر داده شد."
#: taiga/events/events.py:156
msgid "Sprint deleted"
msgstr ""
msgstr "سرعتی حذف شد."
#: taiga/events/events.py:158
msgid "Sprint: {}"
msgstr ""
msgstr "سرعتی: {}"
#: taiga/export_import/api.py:127
msgid "We needed at least one role"
@ -1124,7 +1134,7 @@ msgid "The payload is not a valid json"
msgstr "داده‌ها یک JSON معتبر نیستند"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "پروژه وجود ندارد"
@ -1774,27 +1784,27 @@ msgstr "کاربر می‌بایست قبلاً از اعضای پروژه بو
#: taiga/projects/api.py:642
msgid "You can't delete user story due date by default"
msgstr ""
msgstr "شما سر رسید گزارش کاربر را نمی توانید بصورت عادی حذف کنید."
#: taiga/projects/api.py:658
msgid "Project already have due dates"
msgstr ""
msgstr "پروژه موعد سر رسید دارد."
#: taiga/projects/api.py:718
msgid "You can't delete task due date by default"
msgstr ""
msgstr "شما سر رسید وظیفه کاربر را نمی توانید بصورت عادی حذف کنید."
#: taiga/projects/api.py:734
msgid "Project already have task due dates"
msgstr ""
msgstr "پروژه موعد سر رسید وظیفه دارد."
#: taiga/projects/api.py:858
msgid "You can't delete issue due date by default"
msgstr ""
msgstr "شما سر رسید موضوع کاربر را نمی توانید بصورت عادی حذف کنید."
#: taiga/projects/api.py:874
msgid "Project already have issue due dates"
msgstr ""
msgstr "پروژه موعد سر رسید موضوع دارد."
#: taiga/projects/api.py:1023
msgid ""
@ -2043,11 +2053,11 @@ msgstr "موردی با همین نام وجود دارد."
#: taiga/projects/due_dates/models.py:21
msgid "due date"
msgstr ""
msgstr "موعد سر رسید"
#: taiga/projects/due_dates/models.py:24
msgid "reason for the due date"
msgstr ""
msgstr "دلیل موعد سر رسید."
#: taiga/projects/epics/api.py:94
msgid "You don't have permissions to set this status to this epic."
@ -2204,7 +2214,7 @@ msgstr "آزاد شد"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:164
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:175
msgid "Not set"
msgstr ""
msgstr "تنظیم نشده"
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:286
#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:91
@ -2559,12 +2569,12 @@ msgstr "ارزش"
#: taiga/projects/models.py:614 taiga/projects/models.py:671
#: taiga/projects/models.py:789
msgid "by default"
msgstr ""
msgstr "بصورت عمومی"
#: taiga/projects/models.py:618 taiga/projects/models.py:675
#: taiga/projects/models.py:793
msgid "days to due"
msgstr ""
msgstr "روز تا موعد"
#: taiga/projects/models.py:823
msgid "default owner's role"
@ -2584,7 +2594,7 @@ msgstr "وضعیت‌های استوری‌های کاربری"
#: taiga/projects/models.py:849
msgid "us duedates"
msgstr ""
msgstr "موعد ما"
#: taiga/projects/models.py:850 taiga/projects/userstories/models.py:45
#: taiga/projects/userstories/models.py:78
@ -2597,7 +2607,7 @@ msgstr "وضعیت‌های وظایف"
#: taiga/projects/models.py:852
msgid "task duedates"
msgstr ""
msgstr "موعد وظیفه ها"
#: taiga/projects/models.py:853
msgid "issue statuses"
@ -2609,7 +2619,7 @@ msgstr "انواع موضوعات"
#: taiga/projects/models.py:855
msgid "issue duedates"
msgstr ""
msgstr "موعد موضوعات"
#: taiga/projects/models.py:856
msgid "priorities"
@ -3655,15 +3665,15 @@ msgstr "این رنگ یک کد HEX معتبر نیست."
msgid "The tag doesn't exist."
msgstr "تگ موجود نیست."
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "دسترسی لازم برای تنظیم پیشرفت این وظیفه را ندارید."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "دسترسی لازم برای تنظیم استوری کاربری این وظیفه را ندارید."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "دسترسی لازم برای تعیین وضعیت این وظیفه را ندارید."
@ -4213,7 +4223,7 @@ msgstr "؟"
#. Translators: User story point value (value = 0)
#: taiga/projects/translations.py:47
msgid "0"
msgstr "۰"
msgstr "0"
#. Translators: User story point value (value = 0.5)
#: taiga/projects/translations.py:49
@ -4453,7 +4463,7 @@ msgstr "تاریخ تکمیل"
#: taiga/projects/userstories/models.py:102
msgid "assigned users"
msgstr ""
msgstr "کاربران متصل شده"
#: taiga/projects/userstories/models.py:111
msgid "generated from issue"
@ -4716,11 +4726,11 @@ msgstr "تاریخ عضویت"
#: taiga/users/models.py:155
msgid "accepted terms"
msgstr ""
msgstr "قبول شرایط"
#: taiga/users/models.py:156
msgid "new terms read"
msgstr ""
msgstr "خواندن شرایط جدید"
#: taiga/users/models.py:158
msgid "default language"
@ -4964,7 +4974,7 @@ msgstr "نام کاربری نامعتبر. نام کاربری دیگری ان
#: taiga/users/validators.py:73
msgid "Read new terms has to be true'"
msgstr ""
msgstr "خواندن شرایط جدید باید صحیح باشد"
#: taiga/userstorage/api.py:53
msgid ""
@ -5006,3 +5016,7 @@ msgstr "هدرهای پاسخ"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "مدت زمان"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr "آی پی آدرس غیر مجاز"

View File

@ -11,8 +11,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@ -199,8 +199,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Estetty elementti"
@ -489,6 +489,14 @@ msgstr ""
"\n"
"Kommentti: %(comment)s"
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1078,7 +1086,7 @@ msgid "The payload is not a valid json"
msgstr "The payload is not a valid json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Projektia ei löydy"
@ -3445,15 +3453,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr ""
@ -4705,3 +4713,7 @@ msgstr "response headers"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duration"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -32,8 +32,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@ -231,8 +231,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Élément bloqué"
@ -528,6 +528,14 @@ msgstr ""
" Commentaire : %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1131,7 +1139,7 @@ msgid "The payload is not a valid json"
msgstr "Le payload n'est pas un json valide"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Le projet n'existe pas"
@ -3439,15 +3447,15 @@ msgstr "La couleur n'est pas un code HEX valide."
msgid "The tag doesn't exist."
msgstr "Ce mot-clé n'existe pas."
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "Vous n'avez pas la permission d'affecter ce sprint à cette tâche."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "Vous n'avez pas la permission d'affecter ce récit à cette tâche."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème."
@ -4762,3 +4770,7 @@ msgstr "en-têtes de la réponse"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "durée"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -18,8 +18,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@ -211,8 +211,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Elemento bloccato"
@ -532,6 +532,14 @@ msgstr ""
"\n"
"Commento: %(comment)s"
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1205,7 +1213,7 @@ msgid "The payload is not a valid json"
msgstr "Il carico non è un json valido"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Il progetto non esiste"
@ -3886,16 +3894,16 @@ msgstr "Il colore non e' un codice HEX valido."
msgid "The tag doesn't exist."
msgstr "Il tag non esiste."
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
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:110
#: taiga/projects/tasks/api.py:111
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:113
#: taiga/projects/tasks/api.py:114
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."
@ -5304,3 +5312,7 @@ msgstr "header della risposta"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "durata"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -10,13 +10,14 @@
# Shun Yanaura <metroplexity@gmail.com>, 2016
# Suguru Sato <usagi.vs.tanuki@gmail.com>, 2016
# Tomonori Tanabe <tanb+github@me.com>, 2015
# Masaki Honda <mh35jp@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-12-28 01:50+0000\n"
"Last-Translator: Masaki Honda <mh35jp@gmail.com>\n"
"Language-Team: Japanese (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ja/)\n"
"MIME-Version: 1.0\n"
@ -31,7 +32,7 @@ msgstr "パブリックなレジスタは無効です。"
#: taiga/auth/api.py:93
msgid "You must accept our terms of service and privacy policy"
msgstr ""
msgstr "利用規約とプライバシーポリシーに同意する必要があります"
#: taiga/auth/api.py:102
msgid "invalid register type"
@ -207,8 +208,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "ブロックされた要素"
@ -502,6 +503,14 @@ msgstr ""
"\n"
"コメント: %(comment)s"
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1109,7 +1118,7 @@ msgid "The payload is not a valid json"
msgstr "ペイロードは有効なjsonではありません。"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "プロジェクトは存在していません。"
@ -3248,15 +3257,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr ""
@ -4391,3 +4400,7 @@ msgstr "レスポンスヘッダー"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "期限"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -15,8 +15,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Korean (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ko/)\n"
@ -208,8 +208,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "차단된 엘리먼트"
@ -504,6 +504,14 @@ msgstr ""
" 댓글: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1108,7 +1116,7 @@ msgid "The payload is not a valid json"
msgstr "페이로드의 json이 유효하지 않습니다."
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "프로젝트가 존재하지 않습니다."
@ -3645,15 +3653,15 @@ msgstr "색이 유효하지 않은 HEX색 입니다."
msgid "The tag doesn't exist."
msgstr "태그가 존재하지 않습니다."
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "이 태스크의 스프린트를 설정할 권한이 없습니다."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "이 태스크의 유저 스토리를 설정할 권한이 없습니다."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "이 태스크의 상태를 설정할 권한이 없습니다."
@ -5003,3 +5011,7 @@ msgstr "응답 헤더"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "지속시간"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Norwegian Bokmål (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/nb/)\n"
@ -198,8 +198,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Blokkert element"
@ -490,6 +490,14 @@ msgstr ""
" Kommentar: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -993,7 +1001,7 @@ msgid "The payload is not a valid json"
msgstr "Payloaden er ikke gyldig json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Prosjektet eksisterer ikke"
@ -3126,16 +3134,16 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "Du har ikke tillatelse til å sette denne sprinten til denne oppgaven."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Du har ikke tillatelse til å sette denne brukerhistorien til denne oppgaven."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "Du har ikke tillatelse til å sette denne statusen til denne oppgaven."
@ -4298,3 +4306,7 @@ msgstr ""
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "varighet"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -6,13 +6,14 @@
# Translators:
# Dajo Hein, 2015
# Haroun Pacquee, 2015
# Joannes Anthonius Rommers <ja.rommers@lge.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-11-02 07:56+0000\n"
"Last-Translator: Joannes Anthonius Rommers <ja.rommers@lge.com>\n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
"MIME-Version: 1.0\n"
@ -27,7 +28,7 @@ msgstr "Publieke registratie is uitgeschakeld."
#: taiga/auth/api.py:93
msgid "You must accept our terms of service and privacy policy"
msgstr ""
msgstr "U moet onze servicevoorwaarden en ons privacybeleid accepteren"
#: taiga/auth/api.py:102
msgid "invalid register type"
@ -55,7 +56,7 @@ msgstr "Gebruiker is al geregistreerd."
#: taiga/auth/services.py:141
msgid "This user is already a member of the project."
msgstr ""
msgstr "Deze gebruiker is al lid van het project."
#: taiga/auth/services.py:165
msgid "Error on creating new user."
@ -105,7 +106,7 @@ msgstr ""
#: taiga/base/api/fields.py:638
msgid "You email domain is not allowed"
msgstr ""
msgstr "Jou e-mail domein is niet toegestaan"
#: taiga/base/api/fields.py:647
msgid "Enter a valid email address."
@ -207,11 +208,11 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
msgstr "Geblokkeerd element"
#: taiga/base/api/pagination.py:228
msgid "Page is not 'last', nor can it be converted to an int."
@ -364,7 +365,7 @@ msgstr "Preconditie fout"
#: taiga/base/exceptions.py:219
msgid "No room left for more projects."
msgstr ""
msgstr "Er is geen ruimte over voor meer projecten."
#: taiga/base/filters.py:105 taiga/base/filters.py:526
msgid "Error in filter params types."
@ -486,6 +487,9 @@ msgid ""
"%(comment)s</p>\n"
" "
msgstr ""
"\n"
"<h3>commentaar:</h3>\n"
"<p>%(comment)s</p>"
#: taiga/base/templates/emails/updates-body-text.jinja:6
#, python-format
@ -498,85 +502,93 @@ msgstr ""
" Commentaar: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr "Host toegang fout"
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr "IP adres fout"
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
msgstr "User story aangemaakt"
#: taiga/events/events.py:109
msgid "User story changed"
msgstr ""
msgstr "User story gewijzigd"
#: taiga/events/events.py:112
msgid "User story deleted"
msgstr ""
msgstr "User story verwijderd"
#: taiga/events/events.py:114
msgid "US #{} - {}"
msgstr ""
msgstr "US #{} - {}"
#: taiga/events/events.py:117
msgid "Task created"
msgstr ""
msgstr "Taak aangemaakt"
#: taiga/events/events.py:120
msgid "Task changed"
msgstr ""
msgstr "Taak gewijzigd"
#: taiga/events/events.py:123
msgid "Task deleted"
msgstr ""
msgstr "Taak verwijderd"
#: taiga/events/events.py:125
msgid "Task #{} - {}"
msgstr ""
msgstr "Taak #{} - {}"
#: taiga/events/events.py:128
msgid "Issue created"
msgstr ""
msgstr "Probleem aangemaakt"
#: taiga/events/events.py:131
msgid "Issue changed"
msgstr ""
msgstr "Probleem gewijzigd"
#: taiga/events/events.py:134
msgid "Issue deleted"
msgstr ""
msgstr "Probleem verwijderd"
#: taiga/events/events.py:136
msgid "Issue: #{} - {}"
msgstr ""
msgstr "Probleem: #{} - {}"
#: taiga/events/events.py:139
msgid "Wiki Page created"
msgstr ""
msgstr "Wiki Pagina aangemaakt"
#: taiga/events/events.py:142
msgid "Wiki Page changed"
msgstr ""
msgstr "Wiki Pagina gewijzigd"
#: taiga/events/events.py:145
msgid "Wiki Page deleted"
msgstr ""
msgstr "Wiki Pagina verwijderd"
#: taiga/events/events.py:147
msgid "Wiki Page: {}"
msgstr ""
msgstr "Wiki pagina: {}"
#: taiga/events/events.py:150
msgid "Sprint created"
msgstr ""
msgstr "Sprint aangemaakt"
#: taiga/events/events.py:153
msgid "Sprint changed"
msgstr ""
msgstr "Sprint gewijzigd"
#: taiga/events/events.py:156
msgid "Sprint deleted"
msgstr ""
msgstr "Sprint verwijderd"
#: taiga/events/events.py:158
msgid "Sprint: {}"
msgstr ""
msgstr "Sprint: {}"
#: taiga/export_import/api.py:127
msgid "We needed at least one role"
@ -613,23 +625,23 @@ msgstr "fout bij importeren van standaard projectattributen waarden"
#: taiga/export_import/services/store.py:779
msgid "error importing custom attributes"
msgstr "fout bij importeren eigen attributen"
msgstr "fout bij het importeren van aangepaste attributen"
#: taiga/export_import/services/store.py:783
msgid "error importing sprints"
msgstr "fout bij importeren sprints"
msgstr "fout bij het importeren van sprints"
#: taiga/export_import/services/store.py:787
msgid "error importing issues"
msgstr "fout bij importeren issues"
msgstr "fout bij het importeren van problemen"
#: taiga/export_import/services/store.py:791
msgid "error importing user stories"
msgstr "fout bij importeren user stories"
msgstr "fout bij het importeren van user stories"
#: taiga/export_import/services/store.py:795
msgid "error importing epics"
msgstr ""
msgstr "fout bij het importeren van epics"
#: taiga/export_import/services/store.py:799
msgid "error importing tasks"
@ -653,7 +665,7 @@ msgstr "fout bij importeren tijdlijnen"
#: taiga/export_import/services/store.py:837
msgid "unexpected error importing project"
msgstr ""
msgstr "onverwachte fout tijdens het importeren van het project"
#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
msgid "Error generating project dump"
@ -685,11 +697,11 @@ msgstr "Fout bij laden project dump"
#: taiga/export_import/tasks.py:121
msgid "Error loading your project dump file"
msgstr ""
msgstr "Fout tijdens het laden van de project dump file"
#: taiga/export_import/tasks.py:135
msgid " -- no detail info --"
msgstr ""
msgstr "-- geen gedetailleerde informatie --"
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
#, python-format
@ -885,7 +897,7 @@ msgstr "Naam gedupliceerd voor het project"
#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70
#: taiga/external_apps/api.py:77
msgid "Authentication required"
msgstr ""
msgstr "Authenticatie verreist"
#: taiga/external_apps/models.py:35
#: taiga/projects/custom_attributes/models.py:36
@ -903,11 +915,11 @@ msgstr "naam"
#: taiga/external_apps/models.py:37
msgid "Icon url"
msgstr ""
msgstr "Icoon url"
#: taiga/external_apps/models.py:38
msgid "web"
msgstr ""
msgstr "web"
#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:62
#: taiga/projects/custom_attributes/models.py:37
@ -921,17 +933,17 @@ msgstr "omschrijving"
#: taiga/external_apps/models.py:41
msgid "Next url"
msgstr ""
msgstr "Volgende url"
#: taiga/external_apps/models.py:55 taiga/projects/contact/models.py:26
#: taiga/projects/likes/models.py:31 taiga/projects/notifications/models.py:88
#: taiga/projects/votes/models.py:52
msgid "user"
msgstr ""
msgstr "gebruiker"
#: taiga/external_apps/models.py:59
msgid "application"
msgstr ""
msgstr "applicatie"
#: taiga/feedback/models.py:25 taiga/users/models.py:147
msgid "full name"
@ -1023,7 +1035,7 @@ msgid "The payload is not a valid json"
msgstr "De payload is geen geldige json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Het project bestaat niet"
@ -3176,15 +3188,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr ""
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr ""
@ -4350,3 +4362,7 @@ msgstr "response headers"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duur"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -13,8 +13,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@ -204,8 +204,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Element zablokowany"
@ -502,6 +502,14 @@ msgstr ""
" Komentarz: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1109,7 +1117,7 @@ msgid "The payload is not a valid json"
msgstr "Źródło nie jest prawidłowym plikiem json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Projekt nie istnieje"
@ -3505,16 +3513,16 @@ msgstr "Nieprawidłowy kolor w systemie HEX."
msgid "The tag doesn't exist."
msgstr "Tag nie istnieje"
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
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:110
#: taiga/projects/tasks/api.py:111
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:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania"
@ -4813,3 +4821,7 @@ msgstr "nagłówki odpowiedzi"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "czas trwania"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -18,6 +18,7 @@
# Lucas Boscaini <lucasboscaini@gmail.com>, 2017
# Mairieli Wessel <mairieliw@alunos.utfpr.edu.br>, 2016
# Marlon Carvalho <m.lopes@qiwi.com>, 2015
# Michael Douglas Meneses de Souza <michael.douglas.meneses.2@gmail.com>, 2018
# Michel Wilhelm <michelwilhelm@gmail.com>, 2016
# pedromvm <pedromvm@gmail.com>, 2015
# Pedro Rangel Raft <me@pedroraft.com>, 2017
@ -29,9 +30,10 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-11-17 17:51+0000\n"
"Last-Translator: Michael Douglas Meneses de Souza <michael.douglas."
"meneses.2@gmail.com>\n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/pt_BR/)\n"
"MIME-Version: 1.0\n"
@ -46,7 +48,7 @@ msgstr "Registro público está desabilitado. "
#: taiga/auth/api.py:93
msgid "You must accept our terms of service and privacy policy"
msgstr ""
msgstr "Você deve aceitar os termos do serviço e a política de privacidade"
#: taiga/auth/api.py:102
msgid "invalid register type"
@ -217,8 +219,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Elemento bloqeado"
@ -515,85 +517,93 @@ msgstr ""
" Comentário: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
msgstr "História de Usuário criada"
#: taiga/events/events.py:109
msgid "User story changed"
msgstr ""
msgstr "História de Usuário alterada"
#: taiga/events/events.py:112
msgid "User story deleted"
msgstr ""
msgstr "História de Usuário excluída"
#: taiga/events/events.py:114
msgid "US #{} - {}"
msgstr ""
msgstr "US #{} - {}"
#: taiga/events/events.py:117
msgid "Task created"
msgstr ""
msgstr "Tarefa criada"
#: taiga/events/events.py:120
msgid "Task changed"
msgstr ""
msgstr "Tarefa alterada"
#: taiga/events/events.py:123
msgid "Task deleted"
msgstr ""
msgstr "Tarefa excluída"
#: taiga/events/events.py:125
msgid "Task #{} - {}"
msgstr ""
msgstr "Tarefa #{} - {}"
#: taiga/events/events.py:128
msgid "Issue created"
msgstr ""
msgstr "Issue criada"
#: taiga/events/events.py:131
msgid "Issue changed"
msgstr ""
msgstr "Issue alterada"
#: taiga/events/events.py:134
msgid "Issue deleted"
msgstr ""
msgstr "Issue excluída"
#: taiga/events/events.py:136
msgid "Issue: #{} - {}"
msgstr ""
msgstr "Issue: #{} - {}"
#: taiga/events/events.py:139
msgid "Wiki Page created"
msgstr ""
msgstr "Página Wiki criada"
#: taiga/events/events.py:142
msgid "Wiki Page changed"
msgstr ""
msgstr "Página Wiki alterada"
#: taiga/events/events.py:145
msgid "Wiki Page deleted"
msgstr ""
msgstr "Página Wiki excluída"
#: taiga/events/events.py:147
msgid "Wiki Page: {}"
msgstr ""
msgstr "Página Wiki: {}"
#: taiga/events/events.py:150
msgid "Sprint created"
msgstr ""
msgstr "Sprint criada"
#: taiga/events/events.py:153
msgid "Sprint changed"
msgstr ""
msgstr "Sprint alterada"
#: taiga/events/events.py:156
msgid "Sprint deleted"
msgstr ""
msgstr "Sprint excluída"
#: taiga/events/events.py:158
msgid "Sprint: {}"
msgstr ""
msgstr "Sprint: {}"
#: taiga/export_import/api.py:127
msgid "We needed at least one role"
@ -1128,7 +1138,7 @@ msgid "The payload is not a valid json"
msgstr "A carga não é um json válido"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "O projeto não existe"
@ -1271,7 +1281,7 @@ msgstr "O parâmetro url é necessário"
#: taiga/importers/jira/api.py:158
msgid "Invalid project_type {}"
msgstr ""
msgstr "project_type inválido {}"
#: taiga/importers/jira/api.py:192
msgid "Invalid Jira server configuration."
@ -1510,7 +1520,7 @@ msgstr "Pedido invalido: %s há %s"
#: taiga/importers/trello/importer.py:80 taiga/importers/trello/importer.py:82
#, python-format
msgid "Unauthorized: %s at %s"
msgstr ""
msgstr "Não autorizado: %s até %s"
#: taiga/importers/trello/importer.py:84 taiga/importers/trello/importer.py:86
#, python-format
@ -3616,17 +3626,17 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
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:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"Você não tem permissão para colocar essa história de usuário para essa "
"tarefa."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
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."
@ -4902,3 +4912,7 @@ msgstr "cabeçalhos de resposta"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "duração"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -24,8 +24,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@ -219,8 +219,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Заблокированный элемент"
@ -536,6 +536,14 @@ msgstr ""
" Комментарий: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1140,7 +1148,7 @@ msgid "The payload is not a valid json"
msgstr "Нагрузочный файл не является правильным json-файлом"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Проект не существует"
@ -3639,16 +3647,16 @@ msgstr "Этот некорректный шестнадцатеричный ф
msgid "The tag doesn't exist."
msgstr "Тэг не существует"
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "У вас нет прав, чтобы назначить этот спринт для этой задачи."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr ""
"У вас нет прав, чтобы назначить эту историю от пользователя этой задаче."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "У вас нет прав, чтобы установить этот статус для этой задачи."
@ -5001,3 +5009,7 @@ msgstr "заголовки ответа"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "длительность"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -10,8 +10,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@ -201,8 +201,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Blockerat element"
@ -488,6 +488,14 @@ msgid ""
" "
msgstr ""
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -975,7 +983,7 @@ msgid "The payload is not a valid json"
msgstr "Datasträngen är inte korrekt json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Projektet existerar inte"
@ -3096,15 +3104,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "Du har inte behörighet åt att sätta sprinten till en uppgift"
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "Du har inte behörighet att sätta använderhistorien till en uppgift."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "Du har inte behörighet att sätta status till en uppgift. "
@ -4259,3 +4267,7 @@ msgstr "responstitel"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "varaktighet"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -11,8 +11,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@ -208,8 +208,8 @@ msgstr ""
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "Engellenmiş nesne"
@ -497,6 +497,14 @@ msgstr ""
"\n"
"Yorumlar: %(comment)s"
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1083,7 +1091,7 @@ msgid "The payload is not a valid json"
msgstr ""
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "Proje mevcut değil."
@ -3270,15 +3278,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "Bu görev için sprint ayarlamanız için izniniz yok."
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "Bu görev için kullanıcı hikayesi ayarlama izniniz yok."
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "Bu görev için bu durumu ayarlama izniniz yok."
@ -4430,3 +4438,7 @@ msgstr "cevap başlıkları"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "süre"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
#
# Translators:
# Translators:
# Andy zhan <daliangzao189@126.com>, 2018
# Ares <yangzibin.cn@hotmail.com>, 2017
# gm l <linguangmo@gmail.com>, 2016
# Hanbing Yin <yin_suk@hotmail.com>, 2016
@ -17,16 +18,16 @@
# Yang Yu <yuyangmkdir@yahoo.com>, 2016
# yonee <yangqo@hotmail.com>, 2015
# 5791113 <yonglong.ma@outlook.com>, 2016
# yungang <zangyungang@gmail.com>, 2018
# zangyg <zangyungang@gmail.com>, 2018
# 5791113 <yonglong.ma@outlook.com>, 2016
# 朱坚 <garyzhu2009@gmail.com>, 2017
msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-12-16 07:10+0000\n"
"Last-Translator: Andy zhan <daliangzao189@126.com>\n"
"Language-Team: Chinese Simplified (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hans/)\n"
"MIME-Version: 1.0\n"
@ -206,8 +207,8 @@ msgstr "请上传一张有效的图片。所上传的不是图片或已损坏"
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr "冻结的元素"
@ -518,6 +519,14 @@ msgstr ""
" 评论: %(comment)s\n"
" "
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1118,7 +1127,7 @@ msgid "The payload is not a valid json"
msgstr "内容不是一个合法的JSON"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "项目不存在"
@ -3602,15 +3611,15 @@ msgstr "该颜色不是一个合法的HEX颜色"
msgid "The tag doesn't exist."
msgstr "该标签不存在"
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "你无权对这个任务设置该冲刺任务。"
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "你无权对这个任务设置该用户故事。"
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "你无权对这个任务设置该状态。"
@ -4641,11 +4650,11 @@ msgstr "加入日期"
#: taiga/users/models.py:155
msgid "accepted terms"
msgstr ""
msgstr "已接受的条款"
#: taiga/users/models.py:156
msgid "new terms read"
msgstr ""
msgstr "阅读新条款"
#: taiga/users/models.py:158
msgid "default language"
@ -4886,7 +4895,7 @@ msgstr "无效用户名,请尝试其他的。"
#: taiga/users/validators.py:73
msgid "Read new terms has to be true'"
msgstr ""
msgstr "还没有阅读新条款"
#: taiga/userstorage/api.py:53
msgid ""
@ -4928,3 +4937,7 @@ msgstr "响应Header头"
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "持续时间"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr "IP地址被禁用"

View File

@ -14,8 +14,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-18 17:14+0200\n"
"PO-Revision-Date: 2018-08-10 09:46+0000\n"
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
"Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/"
"taiga-back/language/zh-Hant/)\n"
@ -196,8 +196,8 @@ msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
#: taiga/hooks/api.py:69 taiga/projects/api.py:461 taiga/projects/api.py:494
#: taiga/projects/api.py:970 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:233
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:258
#: taiga/projects/tasks/api.py:283 taiga/projects/userstories/api.py:346
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:259
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
msgid "Blocked element"
msgstr ""
@ -485,6 +485,14 @@ msgstr ""
"\n"
"評論: %(comment)s"
#: taiga/base/utils/urls.py:68
msgid "Host access error"
msgstr ""
#: taiga/base/utils/urls.py:74
msgid "IP Address error"
msgstr ""
#: taiga/events/events.py:106
msgid "User story created"
msgstr ""
@ -1070,7 +1078,7 @@ msgid "The payload is not a valid json"
msgstr "載荷為無效json"
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:210
#: taiga/projects/issues/api.py:142 taiga/projects/tasks/api.py:211
#: taiga/projects/userstories/api.py:290
msgid "The project doesn't exist"
msgstr "專案不存在"
@ -3455,15 +3463,15 @@ msgstr ""
msgid "The tag doesn't exist."
msgstr ""
#: taiga/projects/tasks/api.py:107 taiga/projects/tasks/api.py:116
#: taiga/projects/tasks/api.py:108 taiga/projects/tasks/api.py:117
msgid "You don't have permissions to set this sprint to this task."
msgstr "無權限更動此任務下的衝刺任務"
#: taiga/projects/tasks/api.py:110
#: taiga/projects/tasks/api.py:111
msgid "You don't have permissions to set this user story to this task."
msgstr "無權限更動此務下的使用者故事"
#: taiga/projects/tasks/api.py:113
#: taiga/projects/tasks/api.py:114
msgid "You don't have permissions to set this status to this task."
msgstr "無權限更動此任務下的狀態"
@ -4696,3 +4704,7 @@ msgstr "回應標頭 "
#: taiga/webhooks/models.py:46
msgid "duration"
msgstr "期間"
#: taiga/webhooks/validators.py:42
msgid "Not allowed IP Address"
msgstr ""

View File

@ -37,7 +37,7 @@ class AutolinkExtension(markdown.Extension):
* GitHub only accepts URLs with protocols or "www.", whereas Gruber's regex
accepts things like "foo.com/bar".
"""
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
url_re = r'(?i)\b((?:(?:ftp|https?)://|www\d{0,3}[.])([^\s<>]+))'
autolink = AutolinkPattern(url_re, md)
md.inlinePatterns.add('gfm-autolink', autolink, '_end')

View File

@ -19,7 +19,7 @@ class AutomailPattern(markdown.inlinepatterns.Pattern):
class AutomailExtension(markdown.Extension):
"""An extension that turns all email addresses into links."""
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
mail_re = r'\b(?i)([a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]+)\b'
automail = AutomailPattern(mail_re, md)
md.inlinePatterns.add('gfm-automail', automail, '_end')

View File

@ -152,7 +152,7 @@ EMOJIS_SET = {
class EmojifyExtension(Extension):
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
md.registerExtension(self)
md.preprocessors.add('emojify',
EmojifyPreprocessor(md),

View File

@ -30,7 +30,7 @@ from markdown.util import etree, AtomicString
class MentionsExtension(Extension):
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
MENTION_RE = r"(@)([\w.-]+)"
mentionsPattern = MentionsPattern(MENTION_RE)
mentionsPattern.md = md

View File

@ -36,7 +36,7 @@ class TaigaReferencesExtension(Extension):
self.project = project
return super().__init__(*args, **kwargs)
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
TAIGA_REFERENCE_RE = r'(?<=^|(?<=[^a-zA-Z0-9-\[]))#(\d+)'
referencesPattern = TaigaReferencesPattern(TAIGA_REFERENCE_RE, self.project)
referencesPattern.md = md

View File

@ -28,6 +28,6 @@ class SemiSaneListExtension(markdown.Extension):
newlines.
"""
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
md.parser.blockprocessors['olist'] = SemiSaneOListProcessor(md.parser)
md.parser.blockprocessors['ulist'] = SemiSaneUListProcessor(md.parser)

View File

@ -31,7 +31,7 @@ class SpacedLinkExtension(markdown.Extension):
extension adds such support.
"""
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
md.inlinePatterns["link"] = \
markdown.inlinepatterns.LinkPattern(SPACED_LINK_RE, md)
md.inlinePatterns["reference"] = \

View File

@ -14,6 +14,6 @@ class StrikethroughExtension(markdown.Extension):
For example: ``~~strike~~``.
"""
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
pattern = markdown.inlinepatterns.SimpleTagPattern(STRIKE_RE, 'del')
md.inlinePatterns.add('gfm-strikethrough', pattern, '_end')

View File

@ -28,7 +28,7 @@ from taiga.front.templatetags.functions import resolve
class TargetBlankLinkExtension(markdown.Extension):
"""An extension that add target="_blank" to all external links."""
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
md.treeprocessors.add("target_blank_links",
TargetBlankLinksTreeprocessor(md),
"<prettify")

View File

@ -34,7 +34,7 @@ class WikiLinkExtension(Extension):
self.project = project
return super().__init__(*args, **kwargs)
def extendMarkdown(self, md, md_globals):
def extendMarkdown(self, md):
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
md.inlinePatterns.add("wikilinks",
WikiLinksPattern(md, WIKILINK_RE, self.project),

View File

@ -45,6 +45,7 @@ from taiga.projects.epics.models import Epic
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.issues.models import Issue
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
from taiga.projects.notifications.apps import signal_members_added
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
@ -112,8 +113,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
qs = project_utils.attach_notify_policies(qs)
qs = project_utils.attach_is_fan(qs, user=self.request.user)
qs = project_utils.attach_my_role_permissions(qs, user=self.request.user)
qs = project_utils.attach_my_role_permissions(qs, user=self.request.user)
qs = project_utils.attach_closed_milestones(qs)
qs = project_utils.attach_my_homepage(qs, user=self.request.user)
else:
qs = project_utils.attach_extra_info(qs, user=self.request.user)
@ -980,6 +981,10 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
invitation_extra_text=invitation_extra_text,
callback=self.post_save,
precall=self.pre_save)
signal_members_added.send(sender=self.__class__,
user=self.request.user,
project=project,
new_members=members)
except exc.ValidationError as err:
return response.BadRequest(err.message_dict)

View File

@ -53,17 +53,17 @@ def connect_memberships_signals():
sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
# On membership object is deleted, update notify policies of all objects relation.
signals.post_save.connect(handlers.create_notify_policy,
# On membership object is created, reorder and create notify policies
signals.post_save.connect(handlers.membership_post_save,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
dispatch_uid='membership_post_save')
def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
dispatch_uid='membership_post_save')
## US Statuses Signals

View File

@ -15,12 +15,13 @@
from django.conf import settings
from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.base.utils.thumbnails import get_thumbnail_url, get_thumbnail
def get_timeline_image_thumbnail_url(attachment):
def get_timeline_image_thumbnail_name(attachment):
if attachment.attached_file:
return get_thumbnail_url(attachment.attached_file, settings.THN_ATTACHMENT_TIMELINE)
thumbnail = get_thumbnail(attachment.attached_file, settings.THN_ATTACHMENT_TIMELINE)
return thumbnail.name if thumbnail else None
return None
@ -29,6 +30,7 @@ def get_card_image_thumbnail_url(attachment):
return get_thumbnail_url(attachment.attached_file, settings.THN_ATTACHMENT_CARD)
return None
def get_attachment_image_preview_url(attachment):
if attachment.attached_file:
return get_thumbnail_url(attachment.attached_file, settings.THN_ATTACHMENT_PREVIEW)

View File

@ -24,11 +24,17 @@ MULTILINE_TYPE = "multiline"
RICHTEXT_TYPE = "richtext"
DATE_TYPE = "date"
URL_TYPE = "url"
DROPDOWN_TYPE = "dropdown"
CHECKBOX_TYPE = "checkbox"
NUMBER_TYPE = "number"
TYPES_CHOICES = (
(TEXT_TYPE, _("Text")),
(MULTILINE_TYPE, _("Multi-Line Text")),
(RICHTEXT_TYPE, _("Rich text")),
(DATE_TYPE, _("Date")),
(URL_TYPE, _("Url"))
(URL_TYPE, _("Url")),
(DROPDOWN_TYPE, _("Dropdown")),
(CHECKBOX_TYPE, _("Checkbox")),
(NUMBER_TYPE, _("Number")),
)

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-10-22 16:24
from __future__ import unicode_literals
import django.core.serializers.json
from django.db import migrations, models
import taiga.base.db.models.fields.json
class Migration(migrations.Migration):
dependencies = [
('custom_attributes', '0012_auto_20161201_1628'),
]
operations = [
migrations.AddField(
model_name='epiccustomattribute',
name='extra',
field=taiga.base.db.models.fields.json.JSONField(blank=True, default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
migrations.AddField(
model_name='issuecustomattribute',
name='extra',
field=taiga.base.db.models.fields.json.JSONField(blank=True, default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
migrations.AddField(
model_name='taskcustomattribute',
name='extra',
field=taiga.base.db.models.fields.json.JSONField(blank=True, default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
migrations.AddField(
model_name='userstorycustomattribute',
name='extra',
field=taiga.base.db.models.fields.json.JSONField(blank=True, default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True),
),
migrations.AlterField(
model_name='epiccustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown')], default='text', max_length=16, verbose_name='type'),
),
migrations.AlterField(
model_name='issuecustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown')], default='text', max_length=16, verbose_name='type'),
),
migrations.AlterField(
model_name='taskcustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown')], default='text', max_length=16, verbose_name='type'),
),
migrations.AlterField(
model_name='userstorycustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown')], default='text', max_length=16, verbose_name='type'),
),
]

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-10-25 07:11
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('custom_attributes', '0013_auto_20181022_1624'),
]
operations = [
migrations.AlterField(
model_name='epiccustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown'), ('checkbox', 'Checkbox'), ('number', 'Number')], default='text', max_length=16, verbose_name='type'),
),
migrations.AlterField(
model_name='issuecustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown'), ('checkbox', 'Checkbox'), ('number', 'Number')], default='text', max_length=16, verbose_name='type'),
),
migrations.AlterField(
model_name='taskcustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown'), ('checkbox', 'Checkbox'), ('number', 'Number')], default='text', max_length=16, verbose_name='type'),
),
migrations.AlterField(
model_name='userstorycustomattribute',
name='type',
field=models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('richtext', 'Rich text'), ('date', 'Date'), ('url', 'Url'), ('dropdown', 'Dropdown'), ('checkbox', 'Checkbox'), ('number', 'Number')], default='text', max_length=16, verbose_name='type'),
),
]

View File

@ -41,7 +41,7 @@ class AbstractCustomAttribute(models.Model):
order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order"))
project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss",
verbose_name=_("project"))
extra = JSONField(blank=True, default=None, null=True)
created_date = models.DateTimeField(null=False, blank=False, default=timezone.now,
verbose_name=_("created date"))
modified_date = models.DateTimeField(null=False, blank=False,

View File

@ -32,6 +32,7 @@ class BaseCustomAttributeSerializer(serializers.LightSerializer):
type = Field()
order = Field()
project = Field(attr="project_id")
extra = Field()
created_date = Field()
modified_date = Field()

View File

@ -24,6 +24,8 @@ from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import ReadOnlyListViewSet
from taiga.mdrender.service import render as mdrender
from taiga.projects.notifications import services as notifications_services
from taiga.projects.notifications.apps import signal_mentions
from . import permissions
from . import serializers
@ -57,6 +59,11 @@ class HistoryViewSet(ReadOnlyListViewSet):
return response.Ok(serializer.data)
def _get_new_mentions(self, obj: object, old_comment: str, new_comment: str):
old_mentions = notifications_services.get_mentions(obj.project, old_comment)
submitted_mentions = notifications_services.get_mentions(obj, new_comment)
return list(set(submitted_mentions) - set(old_mentions))
@detail_route(methods=['get'])
def comment_versions(self, request, pk):
obj = self.get_object()
@ -106,11 +113,20 @@ class HistoryViewSet(ReadOnlyListViewSet):
}
})
new_mentions = self._get_new_mentions(obj, history_entry.comment, comment)
history_entry.edit_comment_date = timezone.now()
history_entry.comment = comment
history_entry.comment_html = mdrender(obj.project, comment)
history_entry.comment_versions = comment_versions
history_entry.save()
if new_mentions:
signal_mentions.send(sender=self.__class__,
user=self.request.user,
obj=obj,
mentions=new_mentions)
return response.Ok()
@detail_route(methods=['post'])
@ -165,6 +181,17 @@ class HistoryViewSet(ReadOnlyListViewSet):
self.check_permissions(request, "retrieve", obj)
qs = services.get_history_queryset_by_model_instance(obj)
qs = services.prefetch_owners_in_history_queryset(qs)
history_type = self.request.GET.get('type')
if history_type == 'activity':
qs = qs.filter(diff__isnull=False, comment__exact='').exclude(diff__exact='')
if self.request.GET.get(self.page_kwarg):
qs = qs.order_by("-created_at")
page = self.paginate_queryset(qs)
serializer = self.get_pagination_serializer(page)
return response.Ok(serializer.data)
return self.response_for_queryset(qs)

View File

@ -28,7 +28,7 @@ from taiga.base.utils.iterators import as_tuple
from taiga.base.utils.iterators import as_dict
from taiga.mdrender.service import render as mdrender
from taiga.projects.attachments.services import get_timeline_image_thumbnail_url
from taiga.projects.attachments.services import get_timeline_image_thumbnail_name
import os
@ -198,13 +198,14 @@ def _generic_extract(obj:object, fields:list, default=None) -> dict:
@as_tuple
def extract_attachments(obj) -> list:
for attach in obj.attachments.all():
thumb_url = get_timeline_image_thumbnail_url(attach)
# Force the creation of a thumbnail for the timeline
thumbnail_file = get_timeline_image_thumbnail_name(attach)
yield {"id": attach.id,
"filename": os.path.basename(attach.attached_file.name),
"url": attach.attached_file.url,
"attached_file": str(attach.attached_file),
"thumb_url": thumb_url,
"thumbnail_file": thumbnail_file,
"is_deprecated": attach.is_deprecated,
"description": attach.description,
"order": attach.order}

View File

@ -30,7 +30,7 @@ from .choices import HistoryType
from .choices import HISTORY_TYPE_CHOICES
from taiga.base.utils.diff import make_diff as make_diff_from_dicts
from taiga.projects.custom_attributes.choices import TEXT_TYPE
from taiga.projects.custom_attributes.choices import CHECKBOX_TYPE, NUMBER_TYPE, TEXT_TYPE
# 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
@ -262,13 +262,19 @@ class HistoryEntry(models.Model):
if aid in oldcustattrs and aid in newcustattrs:
changes = make_diff_from_dicts(oldcustattrs[aid], newcustattrs[aid],
excluded_keys=("name"))
newcustattr = newcustattrs.get(aid, {})
if changes:
change_type = newcustattr.get("type", TEXT_TYPE)
old_value = oldcustattrs[aid].get("value", "")
new_value = newcustattrs[aid].get("value", "")
value_diff = get_diff_of_htmls(old_value, new_value)
if change_type in [NUMBER_TYPE, CHECKBOX_TYPE]:
old_value = oldcustattrs[aid].get("value")
new_value = newcustattrs[aid].get("value")
value_diff = [old_value, new_value]
else:
old_value = oldcustattrs[aid].get("value", "")
new_value = newcustattrs[aid].get("value", "")
value_diff = get_diff_of_htmls(old_value,
new_value)
change = {
"name": newcustattr.get("name", ""),
"changes": changes,
@ -279,8 +285,15 @@ class HistoryEntry(models.Model):
elif aid in oldcustattrs and aid not in newcustattrs:
custom_attributes["deleted"].append(oldcustattrs[aid])
elif aid not in oldcustattrs and aid in newcustattrs:
new_value = newcustattrs[aid].get("value", "")
value_diff = get_diff_of_htmls("", new_value)
newcustattr = newcustattrs.get(aid, {})
change_type = newcustattr.get("type", TEXT_TYPE)
if change_type in [NUMBER_TYPE, CHECKBOX_TYPE]:
old_value = None
new_value = newcustattrs[aid].get("value")
value_diff = [old_value, new_value]
else:
new_value = newcustattrs[aid].get("value", "")
value_diff = get_diff_of_htmls("", new_value)
newcustattrs[aid]["value_diff"] = value_diff
custom_attributes["new"].append(newcustattrs[aid])

View File

@ -28,9 +28,12 @@ from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.milestones.models import Milestone
from taiga.projects.mixins.by_ref import ByRefMixin
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.notifications.mixins import AssignedToSignalMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
@ -44,8 +47,10 @@ from . import serializers
from . import validators
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
class IssueViewSet(AssignedToSignalMixin, OCCResourceMixin, VotedResourceMixin,
HistoryResourceMixin, WatchedResourceMixin, ByRefMixin,
TaggedResourceMixin, BlockedByProjectMixin,
ModelCrudViewSet):
validator_class = validators.IssueValidator
queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, )
@ -248,6 +253,22 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
return response.BadRequest(validator.errors)
@list_route(methods=["POST"])
def bulk_update_milestone(self, request, **kwargs):
validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
self.check_permissions(request, "bulk_update_milestone", project)
ret = services.update_issues_milestone_in_bulk(data["bulk_issues"], milestone)
return response.Ok(ret)
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.IssueVotersPermission,)

View File

@ -25,6 +25,11 @@ def connect_issues_signals():
from taiga.projects.tagging import signals as tagging_handlers
from . import signals as handlers
# Cached prev object version
signals.pre_save.connect(handlers.cached_prev_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="cached_prev_issue")
# Finished date
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
sender=apps.get_model("issues", "Issue"),
@ -35,6 +40,14 @@ def connect_issues_signals():
sender=apps.get_model("issues", "Issue"),
dispatch_uid="tags_normalization_issue")
# Open/Close US and Milestone
signals.post_save.connect(handlers.try_to_close_or_open_milestone_when_create_or_edit_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_or_open_milestone_when_create_or_edit_issue")
signals.post_delete.connect(handlers.try_to_close_milestone_when_delete_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_milestone_when_delete_issue")
def connect_issues_custom_attributes_signals():
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
@ -50,11 +63,19 @@ def connect_all_issues_signals():
def disconnect_issues_signals():
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="cached_prev_issue")
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="set_finished_date_when_edit_issue")
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="tags_normalization_issue")
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_or_open_milestone_when_create_or_edit_issue")
signals.post_delete.disconnect(sender=apps.get_model("issues", "Issue"),
dispatch_uid="try_to_close_milestone_when_delete_issue")
def disconnect_issues_custom_attributes_signals():
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),

View File

@ -35,6 +35,7 @@ class IssuePermission(TaigaResourcePermission):
filters_data_perms = AllowAny()
csv_perms = AllowAny()
bulk_create_perms = HasProjectPerm('add_issue')
bulk_update_milestone_perms = HasProjectPerm('modify_issue')
delete_comment_perms= HasProjectPerm('modify_issue')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')

View File

@ -26,6 +26,9 @@ from django.db import connection
from django.utils.translation import ugettext as _
from taiga.base.utils import db, text
from taiga.events import events
from taiga.projects.history.services import take_snapshot
from taiga.projects.issues.apps import (
connect_issues_signals,
disconnect_issues_signals)
@ -72,10 +75,38 @@ def create_issues_in_bulk(bulk_data, callback=None, precall=None, **additional_f
return issues
def snapshot_issues_in_bulk(bulk_data, user):
for issue_data in bulk_data:
try:
issue = models.Issue.objects.get(pk=issue_data['issue_id'])
take_snapshot(issue, user=user)
except models.Issue.DoesNotExist:
pass
def update_issues_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone some issues adding
`bulk_data` should be a list of dicts with the following format:
[{'task_id': <value>}, ...]
"""
issue_milestones = {e["issue_id"]: milestone.id for e in bulk_data}
issue_ids = issue_milestones.keys()
events.emit_event_for_ids(ids=issue_ids,
content_type="issues.issues",
projectid=milestone.project.pk)
db.update_attr_in_bulk_for_ids(issue_milestones, "milestone_id",
model=models.Issue)
return issue_milestones
#####################################################
# CSV
#####################################################
def issues_to_csv(project, queryset):
csv_data = io.StringIO()
fieldnames = ["id", "ref", "subject", "description", "sprint_id", "sprint",

View File

@ -16,9 +16,22 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from contextlib import suppress
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
####################################
# Signals for cached prev task
####################################
# Define the previous version of the task for use it on the post_save handler
def cached_prev_issue(sender, instance, **kwargs):
instance.prev = None
if instance.id:
instance.prev = sender.objects.get(id=instance.id)
####################################
# Signals for set finished date
####################################
@ -30,3 +43,48 @@ def set_finished_date_when_edit_issue(sender, instance, **kwargs):
instance.finished_date = timezone.now()
elif not instance.status.is_closed and instance.finished_date:
instance.finished_date = None
def try_to_close_or_open_milestone_when_create_or_edit_issue(sender, instance, created, **kwargs):
if instance._importing:
return
_try_to_close_or_open_milestone_when_create_or_edit_issue(instance)
def try_to_close_milestone_when_delete_issue(sender, instance, **kwargs):
if instance._importing:
return
_try_to_close_milestone_when_delete_issue(instance)
# Milestone
def _try_to_close_or_open_milestone_when_create_or_edit_issue(instance):
if instance._importing:
return
from taiga.projects.milestones import services as milestone_service
if instance.milestone_id:
if milestone_service.calculate_milestone_is_closed(instance.milestone):
milestone_service.close_milestone(instance.milestone)
else:
milestone_service.open_milestone(instance.milestone)
if instance.prev and instance.prev.milestone_id and instance.prev.milestone_id != instance.milestone_id:
if milestone_service.calculate_milestone_is_closed(instance.prev.milestone):
milestone_service.close_milestone(instance.prev.milestone)
else:
milestone_service.open_milestone(instance.prev.milestone)
def _try_to_close_milestone_when_delete_issue(instance):
if instance._importing:
return
from taiga.projects.milestones import services as milestone_service
with suppress(ObjectDoesNotExist):
if instance.milestone_id and milestone_service.calculate_milestone_is_closed(instance.milestone):
milestone_service.close_milestone(instance.milestone)

View File

@ -15,10 +15,13 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
from taiga.base.fields import PgArrayField
from taiga.projects.milestones.models import Milestone
from taiga.projects.mixins.validators import AssignedToValidator
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
from taiga.projects.notifications.validators import WatchersValidator
@ -43,3 +46,35 @@ class IssuesBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField(required=False)
bulk_issues = serializers.CharField()
# Milestone bulk validators
class _IssueMilestoneBulkValidator(validators.Validator):
issue_id = serializers.IntegerField()
class UpdateMilestoneBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField()
bulk_issues = _IssueMilestoneBulkValidator(many=True)
def validate_milestone_id(self, attrs, source):
filters = {
"project__id": attrs["project_id"],
"id": attrs[source]
}
if not Milestone.objects.filter(**filters).exists():
raise ValidationError(_("The milestone isn't valid for the project"))
return attrs
def validate_bulk_tasks(self, attrs, source):
filters = {
"project__id": attrs["project_id"],
"id__in": [issue["issue_id"] for issue in attrs[source]]
}
if models.Issue.objects.filter(**filters).count() != len(filters["id__in"]):
raise ValidationError(_("All the issues must be from the same project"))
return attrs

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-09-18 13:55
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('projects', '0060_auto_20180614_1338'),
]
operations = [
migrations.AlterUniqueTogether(
name='issuestatus',
unique_together=set([('project', 'name'), ('project', 'slug')]),
),
]

View File

@ -27,11 +27,17 @@ from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.utils import get_object_or_404
from taiga.base.utils.db import get_object_or_none
from taiga.projects.models import Project
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.tasks.validators import UpdateMilestoneBulkValidator as \
TasksUpdateMilestoneValidator
from taiga.projects.issues.validators import UpdateMilestoneBulkValidator as \
IssuesUpdateMilestoneValidator
from . import serializers
from . import services
from . import validators
from . import models
from . import permissions
@ -142,6 +148,69 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
return response.Ok(milestone_stats)
@detail_route(methods=["POST"])
def move_userstories_to_sprint(self, request, pk=None, **kwargs):
milestone = get_object_or_404(models.Milestone, pk=pk)
self.check_permissions(request, "move_related_items", milestone)
validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"])
if data["bulk_stories"]:
self.check_permissions(request, "move_uss_to_sprint", project)
services.update_userstories_milestone_in_bulk(data["bulk_stories"], milestone_result)
services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user)
return response.NoContent()
@detail_route(methods=["POST"])
def move_tasks_to_sprint(self, request, pk=None, **kwargs):
milestone = get_object_or_404(models.Milestone, pk=pk)
self.check_permissions(request, "move_related_items", milestone)
validator = TasksUpdateMilestoneValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"])
if data["bulk_tasks"]:
self.check_permissions(request, "move_tasks_to_sprint", project)
services.update_tasks_milestone_in_bulk(data["bulk_tasks"], milestone_result)
services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user)
return response.NoContent()
@detail_route(methods=["POST"])
def move_issues_to_sprint(self, request, pk=None, **kwargs):
milestone = get_object_or_404(models.Milestone, pk=pk)
self.check_permissions(request, "move_related_items", milestone)
validator = IssuesUpdateMilestoneValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone_result = get_object_or_404(models.Milestone, pk=data["milestone_id"])
if data["bulk_issues"]:
self.check_permissions(request, "move_issues_to_sprint", project)
services.update_issues_milestone_in_bulk(data["bulk_issues"], milestone_result)
services.snapshot_issues_in_bulk(data["bulk_issues"], request.user)
return response.NoContent()
class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
permission_classes = (permissions.MilestoneWatchersPermission,)

View File

@ -33,6 +33,11 @@ class MilestonePermission(TaigaResourcePermission):
stats_perms = HasProjectPerm('view_milestones')
watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
move_related_items_perms = HasProjectPerm('modify_milestone')
move_uss_to_sprint_perms = HasProjectPerm('modify_us')
move_tasks_to_sprint_perms = HasProjectPerm('modify_task')
move_issues_to_sprint_perms = HasProjectPerm('modify_issue')
class MilestoneWatchersPermission(TaigaResourcePermission):
enought_perms = IsProjectAdmin() | IsSuperUser()

View File

@ -16,16 +16,29 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils import timezone
from . import models
from taiga.base.utils import db
from taiga.events import events
from taiga.projects.history.services import take_snapshot
from taiga.projects.services import apply_order_updates
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.userstories.models import UserStory
def calculate_milestone_is_closed(milestone):
return (milestone.user_stories.all().count() > 0 and
all([task.status is not None and task.status.is_closed for task in milestone.tasks.all()]) and
all([user_story.is_closed for user_story in milestone.user_stories.all()]))
all_us_closed = all([user_story.is_closed for user_story in milestone.user_stories.all()])
all_tasks_closed = all([task.status is not None and task.status.is_closed for task in
milestone.tasks.all()])
all_issues_closed = all([issue.is_closed for issue in milestone.issues.all()])
uss_check = milestone.user_stories.all().count() > 0 \
and all_tasks_closed and all_us_closed and all_issues_closed
issues_check = milestone.issues.all().count() > 0 and all_issues_closed \
and all_tasks_closed and all_us_closed
tasks_check = milestone.tasks.all().count() > 0 and all_tasks_closed \
and all_issues_closed and all_us_closed
return uss_check or issues_check or tasks_check
def close_milestone(milestone):
@ -38,3 +51,137 @@ def open_milestone(milestone):
if milestone.closed:
milestone.closed = False
milestone.save(update_fields=["closed",])
def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone and the milestone order of some user stories adding
the extra orders needed to keep consistency.
`bulk_data` should be a list of dicts with the following format:
[{'us_id': <value>, 'order': <value>}, ...]
"""
user_stories = milestone.user_stories.all()
us_orders = {us.id: getattr(us, "sprint_order") for us in user_stories}
new_us_orders = {}
for e in bulk_data:
new_us_orders[e["us_id"]] = e["order"]
# The base orders where we apply the new orders must containg all
# the values
us_orders[e["us_id"]] = e["order"]
apply_order_updates(us_orders, new_us_orders)
us_milestones = {e["us_id"]: milestone.id for e in bulk_data}
user_story_ids = us_milestones.keys()
events.emit_event_for_ids(ids=user_story_ids,
content_type="userstories.userstory",
projectid=milestone.project.pk)
us_instance_list = []
us_values = []
for us_id in user_story_ids:
us = UserStory.objects.get(pk=us_id)
us_instance_list.append(us)
us_values.append({'milestone_id': milestone.id})
db.update_in_bulk(us_instance_list, us_values)
db.update_attr_in_bulk_for_ids(us_orders, "sprint_order", UserStory)
# Updating the milestone for the tasks
Task.objects.filter(
user_story_id__in=[e["us_id"] for e in bulk_data]).update(
milestone=milestone)
return us_orders
def snapshot_userstories_in_bulk(bulk_data, user):
for us_data in bulk_data:
try:
us = UserStory.objects.get(pk=us_data['us_id'])
take_snapshot(us, user=user)
except UserStory.DoesNotExist:
pass
def update_tasks_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone and the milestone order of some tasks adding
the extra orders needed to keep consistency.
`bulk_data` should be a list of dicts with the following format:
[{'task_id': <value>, 'order': <value>}, ...]
"""
tasks = milestone.tasks.all()
task_orders = {task.id: getattr(task, "taskboard_order") for task in tasks}
new_task_orders = {}
for e in bulk_data:
new_task_orders[e["task_id"]] = e["order"]
# The base orders where we apply the new orders must containg all
# the values
task_orders[e["task_id"]] = e["order"]
apply_order_updates(task_orders, new_task_orders)
task_milestones = {e["task_id"]: milestone.id for e in bulk_data}
task_ids = task_milestones.keys()
events.emit_event_for_ids(ids=task_ids,
content_type="tasks.task",
projectid=milestone.project.pk)
task_instance_list = []
task_values = []
for task_id in task_ids:
task = Task.objects.get(pk=task_id)
task_instance_list.append(task)
task_values.append({'milestone_id': milestone.id})
db.update_in_bulk(task_instance_list, task_values)
db.update_attr_in_bulk_for_ids(task_orders, "taskboard_order", Task)
return task_milestones
def snapshot_tasks_in_bulk(bulk_data, user):
for task_data in bulk_data:
try:
task = Task.objects.get(pk=task_data['task_id'])
take_snapshot(task, user=user)
except Task.DoesNotExist:
pass
def update_issues_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone some issues adding
`bulk_data` should be a list of dicts with the following format:
[{'task_id': <value>}, ...]
"""
issue_milestones = {e["issue_id"]: milestone.id for e in bulk_data}
issue_ids = issue_milestones.keys()
events.emit_event_for_ids(ids=issue_ids,
content_type="issues.issues",
projectid=milestone.project.pk)
issues_instance_list = []
issues_values = []
for issue_id in issue_ids:
issue = Issue.objects.get(pk=issue_id)
issues_instance_list.append(issue)
issues_values.append({'milestone_id': milestone.id})
db.update_in_bulk(issues_instance_list, issues_values)
return issue_milestones
def snapshot_issues_in_bulk(bulk_data, user):
for issue_data in bulk_data:
try:
issue = Issue.objects.get(pk=issue_data['issue_id'])
take_snapshot(issue, user=user)
except Issue.DoesNotExist:
pass

View File

@ -19,15 +19,17 @@
from django.utils.translation import ugettext as _
from taiga.base.exceptions import ValidationError
from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.projects.validators import DuplicatedNameInProjectValidator
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.userstories.models import UserStory
from taiga.projects.validators import DuplicatedNameInProjectValidator
from taiga.projects.validators import ProjectExistsValidator
from . import models
class MilestoneExistsValidator:
def validate_sprint_id(self, attrs, source):
def validate_milestone_id(self, attrs, source):
value = attrs[source]
if not models.Milestone.objects.filter(pk=value).exists():
msg = _("There's no milestone with that id")
@ -39,3 +41,28 @@ class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, va
class Meta:
model = models.Milestone
read_only_fields = ("id", "created_date", "modified_date")
# bulk validators
class _UserStoryMilestoneBulkValidator(validators.Validator):
us_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateMilestoneBulkValidator(MilestoneExistsValidator,
ProjectExistsValidator,
validators.Validator):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField()
bulk_stories = _UserStoryMilestoneBulkValidator(many=True)
def validate_bulk_stories(self, attrs, source):
filters = {
"project__id": attrs["project_id"],
"id__in": [us["us_id"] for us in attrs[source]]
}
if UserStory.objects.filter(**filters).count() != len(filters["id__in"]):
raise ValidationError(_("All the user stories must be from the same project"))
return attrs

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2018 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
# 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 <http://www.gnu.org/licenses/>.
default_app_config = "taiga.projects.notifications.apps.NotificationsAppConfig"

View File

@ -17,8 +17,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import Q
from django.utils import timezone
from taiga.base import response
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import GenericViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.models import Project
@ -50,3 +54,53 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
return models.NotifyPolicy.objects.filter(user=self.request.user).filter(
Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user)
).distinct()
class WebNotificationsViewSet(GenericViewSet):
serializer_class = serializers.WebNotificationSerializer
resource_model = models.WebNotification
def check_permissions(self, request, obj=None):
return obj and request.user.is_authenticated() and \
request.user.pk == obj.user_id
def list(self, request):
if self.request.user.is_anonymous():
return response.Ok({})
queryset = models.WebNotification.objects\
.filter(user=self.request.user)
if request.GET.get("only_unread", False):
queryset = queryset.filter(read__isnull=True)
queryset = queryset.order_by('-read', '-created')
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_pagination_serializer(page)
return response.Ok({
"objects": serializer.data,
"total": queryset.count()
})
serializer = self.get_serializer(queryset, many=True)
return response.Ok(serializer.data)
def patch(self, request, *args, **kwargs):
self.check_permissions(request)
resource_id = kwargs.get("resource_id", None)
resource = get_object_or_404(self.resource_model, pk=resource_id)
resource.read = timezone.now()
resource.save()
return response.Ok({})
def post(self, request):
self.check_permissions(request)
models.WebNotification.objects.filter(user=self.request.user)\
.update(read=timezone.now())
return response.Ok()

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2018 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
# 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 <http://www.gnu.org/licenses/>.
from django import dispatch
from django.apps import AppConfig
signal_assigned_to = dispatch.Signal(providing_args=["user", "obj"])
signal_assigned_users = dispatch.Signal(providing_args=["user", "obj",
"new_assigned_users"])
signal_watchers_added = dispatch.Signal(providing_args=["user", "obj",
"new_watchers"])
signal_members_added = dispatch.Signal(providing_args=["user", "project",
"new_members"])
signal_mentions = dispatch.Signal(providing_args=["user", "obj",
"mentions"])
signal_comment = dispatch.Signal(providing_args=["user", "obj",
"watchers"])
signal_comment_mentions = dispatch.Signal(providing_args=["user", "obj",
"mentions"])
class NotificationsAppConfig(AppConfig):
name = "taiga.projects.notifications"
verbose_name = "Notifications"
def ready(self):
from . import signals as handlers
signal_assigned_to.connect(handlers.on_assigned_to)
signal_assigned_users.connect(handlers.on_assigned_users)
signal_watchers_added.connect(handlers.on_watchers_added)
signal_members_added.connect(handlers.on_members_added)
signal_mentions.connect(handlers.on_mentions)
signal_comment.connect(handlers.on_comment)
signal_comment_mentions.connect(handlers.on_comment_mentions)

View File

@ -31,3 +31,22 @@ NOTIFY_LEVEL_CHOICES = (
(NotifyLevel.all, _("All")),
(NotifyLevel.none, _("None")),
)
class WebNotificationType(enum.IntEnum):
assigned = 1
mentioned = 2
added_as_watcher = 3
added_as_member = 4
comment = 5
mentioned_in_comment = 6
WEB_NOTIFICATION_TYPE_CHOICES = (
(WebNotificationType.assigned, _("Assigned")),
(WebNotificationType.mentioned, _("Mentioned")),
(WebNotificationType.added_as_watcher, _("Added as watcher")),
(WebNotificationType.added_as_member, _("Added as member")),
(WebNotificationType.comment, _("Comment")),
(WebNotificationType.mentioned_in_comment, _("Mentioned in comment")),
)

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-10-10 11:24
from __future__ import unicode_literals
from django.conf import settings
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import taiga.base.db.models.fields.json
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('notifications', '0007_notifypolicy_live_notify_level'),
]
operations = [
migrations.CreateModel(
name='WebNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
('read', models.DateTimeField(default=None, null=True)),
('event_type', models.PositiveIntegerField()),
('data', taiga.base.db.models.fields.json.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='web_notifications', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='notifypolicy',
name='web_notify_level',
field=models.BooleanField(default=True),
),
]

View File

@ -29,6 +29,12 @@ from taiga.base.api.utils import get_object_or_404
from taiga.base.fields import WatchersField, MethodField
from taiga.projects.notifications import services
from . apps import signal_assigned_to
from . apps import signal_assigned_users
from . apps import signal_comment
from . apps import signal_comment_mentions
from . apps import signal_mentions
from . apps import signal_watchers_added
from . serializers import WatcherSerializer
@ -47,6 +53,8 @@ class WatchedResourceMixin:
"""
_not_notify = False
_old_watchers = None
_old_mentions = []
@detail_route(methods=["POST"])
def watch(self, request, pk=None):
@ -86,13 +94,38 @@ class WatchedResourceMixin:
# 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.comment, history.owner)
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.
services.send_notifications(obj, history=history)
def update(self, request, *args, **kwargs):
obj = self.get_object_or_none()
if obj and obj.id:
if hasattr(obj, "watchers"):
self._old_watchers = [
watcher.id for watcher in self.get_object().get_watchers()
]
mention_fields = ['description', 'content']
for field_name in mention_fields:
old_mentions = self._get_old_mentions_in_field(obj, field_name)
if not len(old_mentions):
continue
self._old_mentions = old_mentions
return super().update(request, *args, **kwargs)
def post_save(self, obj, created=False):
self.create_web_notifications_for_added_watchers(obj)
self.create_web_notifications_for_mentioned_users(obj)
mentions = self.create_web_notifications_for_mentions_in_comments(obj)
exclude = mentions + [self.request.user.id]
self.create_web_notifications_for_comment(obj, exclude)
self.send_notifications(obj)
super().post_save(obj, created)
@ -100,6 +133,84 @@ class WatchedResourceMixin:
self.send_notifications(obj)
super().pre_delete(obj)
def create_web_notifications_for_comment(self, obj, exclude: list=None):
if "comment" in self.request.DATA:
watchers = [
watcher_id for watcher_id in obj.watchers
if watcher_id not in exclude
]
signal_comment.send(sender=self.__class__,
user=self.request.user,
obj=obj,
watchers=watchers)
def create_web_notifications_for_added_watchers(self, obj):
if not hasattr(obj, "watchers"):
return
new_watchers = [
watcher_id for watcher_id in obj.watchers
if watcher_id not in self._old_watchers
and watcher_id != self.request.user.id
]
signal_watchers_added.send(sender=self.__class__,
user=self.request.user,
obj=obj,
new_watchers=new_watchers)
def create_web_notifications_for_mentioned_users(self, obj):
"""
Detect and notify mentioned users
"""
submitted_mentions = self._get_submitted_mentions(obj)
new_mentions = list(set(submitted_mentions) - set(self._old_mentions))
if new_mentions:
signal_mentions.send(sender=self.__class__,
user=self.request.user,
obj=obj,
mentions=new_mentions)
def create_web_notifications_for_mentions_in_comments(self, obj):
"""
Detect and notify mentioned users
"""
new_mentions_in_comment = self._get_mentions_in_comment(obj)
if new_mentions_in_comment:
signal_comment_mentions.send(sender=self.__class__,
user=self.request.user,
obj=obj,
mentions=new_mentions_in_comment)
return [user.id for user in new_mentions_in_comment]
def _get_submitted_mentions(self, obj):
mention_fields = ['description', 'content']
for field_name in mention_fields:
new_mentions = self._get_new_mentions_in_field(obj, field_name)
if len(new_mentions) > 0:
return new_mentions
return []
def _get_mentions_in_comment(self, obj):
comment = self.request.DATA.get('comment')
if comment:
return services.get_mentions(obj, comment)
return []
def _get_old_mentions_in_field(self, obj, field_name):
if not hasattr(obj, field_name):
return []
return services.get_mentions(obj, getattr(obj, field_name))
def _get_new_mentions_in_field(self, obj, field_name):
value = self.request.DATA.get(field_name)
if not value:
return []
return services.get_mentions(obj, value)
class WatchedModelMixin(object):
"""
@ -274,3 +385,47 @@ class WatchersViewSetMixin:
def get_queryset(self):
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
return resource.get_watchers()
class AssignedToSignalMixin:
_old_assigned_to = None
def pre_save(self, obj):
if obj.id:
self._old_assigned_to = self.get_object().assigned_to
super().pre_save(obj)
def post_save(self, obj, created=False):
if obj.assigned_to and obj.assigned_to != self._old_assigned_to \
and self.request.user != obj.assigned_to:
signal_assigned_to.send(sender=self.__class__,
user=self.request.user,
obj=obj)
super().post_save(obj, created)
class AssignedUsersSignalMixin:
_old_assigned_users = None
def update(self, request, *args, **kwargs):
obj = self.get_object_or_none()
if hasattr(obj, "assigned_users") and obj.id:
self._old_assigned_users = [
user for user in obj.assigned_users.all()
].copy()
result = super().update(request, *args, **kwargs)
if result and obj.assigned_users:
new_assigned_users = [
user for user in obj.assigned_users.all()
if user not in self._old_assigned_users
and user != self.request.user
]
signal_assigned_users.send(sender=self.__class__,
user=self.request.user,
obj=obj,
new_assigned_users=new_assigned_users)
return result

View File

@ -23,6 +23,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from taiga.base.db.models.fields import JSONField
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel
@ -37,6 +38,7 @@ class NotifyPolicy(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="notify_policies")
notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES)
live_notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES, default=NotifyLevel.involved)
web_notify_level = models.BooleanField(default=True, null=False, blank=True)
created_at = models.DateTimeField(default=timezone.now)
modified_at = models.DateTimeField()
@ -94,3 +96,11 @@ class Watched(models.Model):
verbose_name = _("Watched")
verbose_name_plural = _("Watched")
unique_together = ("content_type", "object_id", "user", "project")
class WebNotification(models.Model):
created = models.DateTimeField(default=timezone.now, db_index=True)
read = models.DateTimeField(default=None, null=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="web_notifications")
event_type = models.PositiveIntegerField()
data = JSONField()

View File

@ -16,8 +16,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.contenttypes.models import ContentType
from taiga.base.api import serializers
from taiga.base.fields import Field, DateTimeField, MethodField
from taiga.users.gravatar import get_user_gravatar_id
from taiga.users.models import get_user_model_safe
from taiga.users.services import get_user_photo_url, get_user_big_photo_url
from . import models
@ -27,7 +32,8 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
class Meta:
model = models.NotifyPolicy
fields = ('id', 'project', 'project_name', 'notify_level', "live_notify_level")
fields = ('id', 'project', 'project_name', 'notify_level',
'live_notify_level', 'web_notify_level')
def get_project_name(self, obj):
return obj.project.name
@ -39,3 +45,67 @@ class WatcherSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model_safe()
fields = ('id', 'username', 'full_name')
class WebNotificationSerializer(serializers.ModelSerializer):
class Meta:
model = models.WebNotification
fields = ('id', 'event_type', 'user', 'data', 'created', 'read')
class ProjectSerializer(serializers.LightSerializer):
id = Field()
slug = Field()
name = Field()
class ObjectSerializer(serializers.LightSerializer):
id = Field()
ref = MethodField()
subject = MethodField()
content_type = MethodField()
def get_ref(self, obj):
return obj.ref if hasattr(obj, 'ref') else None
def get_subject(self, obj):
return obj.subject if hasattr(obj, 'subject') else None
def get_content_type(self, obj):
content_type = ContentType.objects.get_for_model(obj)
return content_type.model if content_type else None
class UserSerializer(serializers.LightSerializer):
id = Field()
name = MethodField()
photo = MethodField()
big_photo = MethodField()
gravatar_id = MethodField()
username = Field()
is_profile_visible = MethodField()
date_joined = DateTimeField()
def get_name(self, obj):
return obj.get_full_name()
def get_photo(self, obj):
return get_user_photo_url(obj)
def get_big_photo(self, obj):
return get_user_big_photo_url(obj)
def get_gravatar_id(self, obj):
return get_user_gravatar_id(obj)
def get_is_profile_visible(self, obj):
return obj.is_active and not obj.is_system
class NotificationDataSerializer(serializers.LightDictSerializer):
project = ProjectSerializer()
user = UserSerializer()
class ObjectNotificationSerializer(NotificationDataSerializer):
obj = ObjectSerializer()

View File

@ -44,6 +44,10 @@ from .models import HistoryChangeNotification, Watched
from .squashing import squash_history_entries
def remove_lr_cr(s):
return s.replace("\n", "").replace("\r", "")
def notify_policy_exists(project, user) -> bool:
"""
Check if policy exists for specified project
@ -73,7 +77,8 @@ def create_notify_policy(project, user, level=NotifyLevel.involved,
def create_notify_policy_if_not_exists(project, user,
level=NotifyLevel.involved,
live_level=NotifyLevel.involved):
live_level=NotifyLevel.involved,
web_level=True):
"""
Given a project and user, create notification policy for it.
"""
@ -82,7 +87,11 @@ def create_notify_policy_if_not_exists(project, user,
result = model_cls.objects.get_or_create(
project=project,
user=user,
defaults={"notify_level": level, "live_notify_level": live_level}
defaults={
"notify_level": level,
"live_notify_level": live_level,
"web_notify_level": web_level
}
)
return result[0]
except IntegrityError as e:
@ -95,27 +104,39 @@ 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"):
if not hasattr(obj, "add_watcher"):
return
if not hasattr(obj, "add_watcher"):
mentions = get_object_mentions(obj, comment)
if mentions:
for user in mentions:
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 get_object_mentions(obj: object, comment: str):
"""
Generic implementation for analize model objects and
extract mentions from it.
"""
if not hasattr(obj, "get_project"):
return
texts = (getattr(obj, "description", ""),
getattr(obj, "content", ""),
comment,)
return get_mentions(obj.get_project(), "\n".join(texts))
def get_mentions(project: object, text: str):
from taiga.mdrender.service import render_and_extract
_, data = render_and_extract(obj.get_project(), "\n".join(texts))
_, data = render_and_extract(project, text)
if data["mentions"]:
for user in data["mentions"]:
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)
return data.get("mentions")
def _filter_by_permissions(obj, user):
@ -296,10 +317,11 @@ def send_sync_notifications(notification_id):
msg_id = 'taiga-system'
now = datetime.datetime.now()
project_name = remove_lr_cr(notification.project.name)
format_args = {
"unsubscribe_url": resolve_front_url('settings-mail-notifications'),
"project_slug": notification.project.slug,
"project_name": notification.project.name,
"project_name": project_name,
"msg_id": msg_id,
"time": int(now.timestamp()),
"domain": domain

View File

@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2018 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
# 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 <http://www.gnu.org/licenses/>.
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.utils import timezone
from taiga.events import events
from taiga.events import middleware as mw
from . import choices
from . import models
from . import serializers
def _filter_recipients(project, user, recipients):
notify_policies = models.NotifyPolicy.objects.filter(
user_id__in=recipients,
project=project,
web_notify_level=True).exclude(user_id=user.id).all()
return [notify_policy.user_id for notify_policy in notify_policies]
def _push_to_web_notifications(event_type, data, recipients,
serializer_class=None):
if not serializer_class:
serializer_class = serializers.ObjectNotificationSerializer
serializer = serializer_class(data)
for user_id in recipients:
with transaction.atomic():
models.WebNotification.objects.create(
event_type=event_type.value,
created=timezone.now(),
user_id=user_id,
data=serializer.data,
)
session_id = mw.get_current_session_id()
events.emit_event_for_user_notification(user_id,
session_id=session_id,
event_type=event_type.value,
data=serializer.data)
def on_assigned_to(sender, user, obj, **kwargs):
event_type = choices.WebNotificationType.assigned
data = {
"project": obj.project,
"user": user,
"obj": obj,
}
recipients = _filter_recipients(obj.project, user,
[obj.assigned_to.id])
_push_to_web_notifications(event_type, data, recipients)
def on_assigned_users(sender, user, obj, new_assigned_users, **kwargs):
event_type = choices.WebNotificationType.assigned
data = {
"project": obj.project,
"user": user,
"obj": obj,
}
recipients = _filter_recipients(obj.project, user,
[user.id for user in new_assigned_users])
_push_to_web_notifications(event_type, data, recipients)
def on_watchers_added(sender, user, obj, new_watchers, **kwargs):
event_type = choices.WebNotificationType.added_as_watcher
data = {
"project": obj.project,
"user": user,
"obj": obj,
}
recipients = _filter_recipients(obj.project, user, new_watchers)
_push_to_web_notifications(event_type, data, recipients)
def on_members_added(sender, user, project, new_members, **kwargs):
serializer_class = serializers.NotificationDataSerializer
event_type = choices.WebNotificationType.added_as_member
data = {
"project": project,
"user": user,
}
recipients = _filter_recipients(project, user,
[member.user_id for member in new_members
if member.user_id])
_push_to_web_notifications(event_type, data, recipients, serializer_class)
def on_mentions(sender, user, obj, mentions, **kwargs):
content_type = ContentType.objects.get_for_model(obj)
valid_content_types = ['issue', 'task', 'userstory']
if content_type.model in valid_content_types:
event_type = choices.WebNotificationType.mentioned
data = {
"project": obj.project,
"user": user,
"obj": obj,
}
recipients = _filter_recipients(obj.project, user,
[user.id for user in mentions])
_push_to_web_notifications(event_type, data, recipients)
def on_comment_mentions(sender, user, obj, mentions, **kwargs):
event_type = choices.WebNotificationType.mentioned_in_comment
data = {
"project": obj.project,
"user": user,
"obj": obj,
}
recipients = _filter_recipients(obj.project, user,
[user.id for user in mentions])
_push_to_web_notifications(event_type, data, recipients)
def on_comment(sender, user, obj, watchers, **kwargs):
event_type = choices.WebNotificationType.comment
data = {
"project": obj.project,
"user": user,
"obj": obj,
}
recipients = _filter_recipients(obj.project, user, watchers)
_push_to_web_notifications(event_type, data, recipients)

View File

@ -290,6 +290,8 @@ class ProjectSerializer(serializers.LightSerializer):
is_fan = Field(attr="is_fan_attr")
my_homepage = MethodField()
def get_members(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
@ -374,6 +376,13 @@ class ProjectSerializer(serializers.LightSerializer):
def get_logo_big_url(self, obj):
return services.get_logo_big_thumbnail_url(obj)
def get_my_homepage(self, obj):
assert hasattr(obj, "my_homepage_attr"), "instance must have a my_homepage_attr attribute"
if obj.my_homepage_attr is None:
return False
return obj.my_homepage_attr
class ProjectDetailSerializer(ProjectSerializer):
epic_statuses = Field(attr="epic_statuses_attr")

View File

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or 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 <http://www.gnu.org/licenses/>.
from django.db.models import Q
from taiga.base import response
from taiga.base.api import ModelCrudViewSet, ReadOnlyListViewSet
from taiga.projects.settings.choices import HOMEPAGE_CHOICES
from taiga.projects.models import Project
from . import models
from . import permissions
from . import serializers
from . import services
from . import validators
class UserProjectSettingsViewSet(ModelCrudViewSet):
serializer_class = serializers.UserProjectSettingsSerializer
permission_classes = (permissions.UserProjectSettingsPermission,)
validator_class = validators.UserProjectSettingsValidator
def _build_user_project_settings(self):
projects = Project.objects.filter(
Q(owner=self.request.user) |
Q(memberships__user=self.request.user)
).distinct()
for project in projects:
services.create_user_project_settings_if_not_exists(
project, self.request.user)
def get_queryset(self):
if self.request.user.is_anonymous():
return models.UserProjectSettings.objects.none()
self._build_user_project_settings()
return models.UserProjectSettings.objects.filter(user=self.request.user)\
.filter(
Q(project__owner=self.request.user) |
Q(project__memberships__user=self.request.user)
).distinct()
def list(self, request, *args, **kwargs):
qs = self.get_queryset()
project_id = request.QUERY_PARAMS.get("project", None)
if project_id:
qs = qs.filter(project_id=project_id)
serializer = self.get_serializer(qs, many=True)
return response.Ok(serializer.data)
class SectionsViewSet(ReadOnlyListViewSet):
def list(self, request, *args, **kwargs):
return response.Response(HOMEPAGE_CHOICES)

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or 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 <http://www.gnu.org/licenses/>.
import enum
from django.utils.translation import ugettext_lazy as _
class Section(enum.IntEnum):
timeline = 1
epics = 2
backlog = 3
kanban = 4
issues = 5
wiki = 6
HOMEPAGE_CHOICES = (
(Section.timeline, _("Timeline")),
(Section.epics, _("Epics")),
(Section.backlog, _("Backlog")),
(Section.kanban, _("Kanban")),
(Section.issues, _("Issues")),
(Section.wiki, _("TeamWiki")),
)

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2018-09-24 11:49
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import taiga.projects.settings.choices
class Migration(migrations.Migration):
initial = True
dependencies = [
('projects', '0061_auto_20180918_1355'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProjectSettings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('homepage', models.SmallIntegerField(choices=[(taiga.projects.settings.choices.Section(1), 'Timeline'), (taiga.projects.settings.choices.Section(2), 'Epics'), (taiga.projects.settings.choices.Section(3), 'Backlog'), (taiga.projects.settings.choices.Section(4), 'Kanban'), (taiga.projects.settings.choices.Section(5), 'Issues'), (taiga.projects.settings.choices.Section(6), 'TeamWiki')], default=taiga.projects.settings.choices.Section(1))),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('modified_at', models.DateTimeField()),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_project_settings', to='projects.Project')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_project_settings', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created_at'],
},
),
migrations.AlterUniqueTogether(
name='userprojectsettings',
unique_together=set([('project', 'user')]),
),
]

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or 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 <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.db import models
from django.utils import timezone
from .choices import HOMEPAGE_CHOICES, Section
class UserProjectSettings(models.Model):
"""
This class represents a persistence for
project user notifications preference.
"""
project = models.ForeignKey("projects.Project", related_name="user_project_settings")
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="user_project_settings")
homepage = models.SmallIntegerField(choices=HOMEPAGE_CHOICES,
default=Section.timeline)
created_at = models.DateTimeField(default=timezone.now)
modified_at = models.DateTimeField()
_importing = None
class Meta:
unique_together = ("project", "user",)
ordering = ["created_at"]
def save(self, *args, **kwargs):
if not self._importing or not self.modified_date:
self.modified_at = timezone.now()
return super().save(*args, **kwargs)

View File

@ -16,12 +16,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .testing import *
from taiga.base.api.permissions import (TaigaResourcePermission, IsAuthenticated)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'taiga',
'USERNAME': 'postgres',
}
}
class UserProjectSettingsPermission(TaigaResourcePermission):
retrieve_perms = IsAuthenticated()
create_perms = IsAuthenticated()
update_perms = IsAuthenticated()
partial_update_perms = IsAuthenticated()
destroy_perms = IsAuthenticated()
list_perms = IsAuthenticated()

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or 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 <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers
from . import models
from taiga.projects.settings.utils import get_allowed_sections
class UserProjectSettingsSerializer(serializers.ModelSerializer):
project_name = serializers.SerializerMethodField("get_project_name")
allowed_sections = serializers.SerializerMethodField("get_allowed_sections")
class Meta:
model = models.UserProjectSettings
fields = ('id', 'project', 'project_name', 'homepage', 'allowed_sections')
def get_project_name(self, obj):
return obj.project.name
def get_allowed_sections(self, obj):
return get_allowed_sections(obj)

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or 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 <http://www.gnu.org/licenses/>.
from django.apps import apps
from django.db import IntegrityError
from django.utils.translation import ugettext as _
from taiga.base import exceptions as exc
from taiga.projects.settings.choices import Section
def user_project_settings_exists(project, user) -> bool:
"""
Check if policy exists for specified project
and user.
"""
model_cls = apps.get_model("settings", "UserProjectSettings")
qs = model_cls.objects.filter(project=project,
user=user)
return qs.exists()
def create_user_project_settings(project, user, homepage=Section.timeline):
"""
Given a project and user, create notification policy for it.
"""
model_cls = apps.get_model("settings", "UserProjectSettings")
try:
return model_cls.objects.create(project=project,
user=user,
homepage=homepage)
except IntegrityError as e:
raise exc.IntegrityError(
_("Notify exists for specified user and project")) from e
def create_user_project_settings_if_not_exists(project, user,
homepage=Section.timeline):
"""
Given a project and user, create notification policy for it.
"""
model_cls = apps.get_model("settings", "UserProjectSettings")
try:
result = model_cls.objects.get_or_create(
project=project,
user=user,
defaults={"homepage": homepage}
)
return result[0]
except IntegrityError as e:
raise exc.IntegrityError(
_("Notify exists for specified user and project")) from e

View File

@ -0,0 +1,17 @@
from taiga.permissions.services import is_project_admin, user_has_perm
from taiga.projects.settings.choices import Section
def get_allowed_sections(obj):
sections = [Section.timeline]
active_modules = {'epics': 'view_epics', 'backlog': 'view_us',
'kanban': 'view_us', 'wiki': 'view_wiki_pages',
'issues': 'view_issues'}
for key in active_modules:
module_name = "is_{}_activated".format(key)
if getattr(obj.project, module_name) and \
user_has_perm(obj.user, active_modules[key], obj.project):
sections.append(getattr(Section, key))
return sections

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or 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 <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
from taiga.projects.settings.utils import get_allowed_sections
from . import models
class UserProjectSettingsValidator(validators.ModelValidator):
class Meta:
model = models.UserProjectSettings
read_only_fields = ('id', 'created_at', 'modified_at', 'project',
'user')
def validate_homepage(self, attrs, source):
if attrs[source] not in get_allowed_sections(self.object):
msg = _("You don't have access to this section")
raise ValidationError(msg)
return attrs

View File

@ -18,6 +18,7 @@
from django.apps import apps
from django.conf import settings
from django.db.models import F
from taiga.projects.notifications.services import create_notify_policy_if_not_exists
@ -32,15 +33,21 @@ def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points()
## Notify policy
def membership_post_save(sender, instance, using, **kwargs):
if not instance.user:
return
create_notify_policy_if_not_exists(instance.project, instance.user)
def create_notify_policy(sender, instance, using, **kwargs):
if instance.user:
create_notify_policy_if_not_exists(instance.project, instance.user)
# Set project on top on user projects list
membership = apps.get_model("projects", "Membership")
membership.objects.filter(user=instance.user) \
.update(user_order=F('user_order') + 1)
membership.objects.filter(user=instance.user, project=instance.project)\
.update(user_order=0)
## Project attributes
def project_post_save(sender, instance, created, **kwargs):
"""
Populate new project dependen default data

View File

@ -30,7 +30,9 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.milestones.models import Milestone
from taiga.projects.mixins.by_ref import ByRefMixin
from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.notifications.mixins import AssignedToSignalMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.userstories.models import UserStory
@ -45,8 +47,10 @@ from . import validators
from . import utils as tasks_utils
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
class TaskViewSet(AssignedToSignalMixin, OCCResourceMixin, VotedResourceMixin,
HistoryResourceMixin, WatchedResourceMixin, ByRefMixin,
TaggedResourceMixin, BlockedByProjectMixin,
ModelCrudViewSet):
validator_class = validators.TaskValidator
queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,)
@ -74,6 +78,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
"created_date",
"modified_date",
"assigned_to",
"us_order",
"subject",
"total_voters")
@ -270,6 +275,23 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
return response.Ok(tasks_serialized.data)
@list_route(methods=["POST"])
def bulk_update_milestone(self, request, **kwargs):
validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
if not validator.is_valid():
return response.BadRequest(validator.errors)
data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
self.check_permissions(request, "bulk_update_milestone", project)
ret = services.update_tasks_milestone_in_bulk(data["bulk_tasks"], milestone)
services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user)
return response.Ok(ret)
def _bulk_update_order(self, order_field, request, **kwargs):
validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
if not validator.is_valid():

View File

@ -91,3 +91,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, DueDateM
def __str__(self):
return "({1}) {0}".format(self.ref, self.subject)
@property
def is_closed(self):
return self.status is not None and self.status.is_closed

View File

@ -35,6 +35,7 @@ class TaskPermission(TaigaResourcePermission):
csv_perms = AllowAny()
bulk_create_perms = HasProjectPerm('add_task')
bulk_update_order_perms = HasProjectPerm('modify_task')
bulk_update_milestone_perms = HasProjectPerm('modify_task')
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')

View File

@ -112,6 +112,39 @@ def snapshot_tasks_in_bulk(bulk_data, user):
pass
def update_tasks_milestone_in_bulk(bulk_data: list, milestone: object):
"""
Update the milestone and the milestone order of some tasks adding
the extra orders needed to keep consistency.
`bulk_data` should be a list of dicts with the following format:
[{'task_id': <value>, 'order': <value>}, ...]
"""
tasks = milestone.tasks.all()
task_orders = {task.id: getattr(task, "taskboard_order") for task in tasks}
new_task_orders = {}
for e in bulk_data:
new_task_orders[e["task_id"]] = e["order"]
# The base orders where we apply the new orders must containg all
# the values
task_orders[e["task_id"]] = e["order"]
apply_order_updates(task_orders, new_task_orders)
task_milestones = {e["task_id"]: milestone.id for e in bulk_data}
task_ids = task_milestones.keys()
events.emit_event_for_ids(ids=task_ids,
content_type="tasks.task",
projectid=milestone.project.pk)
db.update_attr_in_bulk_for_ids(task_milestones, "milestone_id",
model=models.Task)
db.update_attr_in_bulk_for_ids(task_orders, "taskboard_order", models.Task)
return task_milestones
#####################################################
# CSV
#####################################################

View File

@ -31,9 +31,9 @@ def cached_prev_task(sender, instance, **kwargs):
instance.prev = sender.objects.get(id=instance.id)
####################################
# Signals for close US and Milestone
####################################
######################################
# Signals for close Task and Milestone
######################################
def try_to_close_or_open_us_and_milestone_when_create_or_edit_task(sender, instance, created, **kwargs):
_try_to_close_or_open_us_when_create_or_edit_task(instance)

View File

@ -153,3 +153,36 @@ class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator
"if it exists, to the same status, user story and/or milestone."))
return attrs
# Milestone bulk validators
class _TaskMilestoneBulkValidator(validators.Validator):
task_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateMilestoneBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField()
bulk_tasks = _TaskMilestoneBulkValidator(many=True)
def validate_milestone_id(self, attrs, source):
filters = {
"project__id": attrs["project_id"],
"id": attrs[source]
}
if not Milestone.objects.filter(**filters).exists():
raise ValidationError(_("The milestone isn't valid for the project"))
return attrs
def validate_bulk_tasks(self, attrs, source):
filters = {
"project__id": attrs["project_id"],
"id__in": [task["task_id"] for task in attrs[source]]
}
if models.Task.objects.filter(**filters).count() != len(filters["id__in"]):
raise ValidationError(_("All the tasks must be from the same project"))
return attrs

View File

@ -39,6 +39,7 @@ from taiga.projects.history.services import take_snapshot
from taiga.projects.milestones.models import Milestone
from taiga.projects.mixins.by_ref import ByRefMixin
from taiga.projects.models import Project, UserStoryStatus
from taiga.projects.notifications.mixins import AssignedUsersSignalMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
@ -55,8 +56,10 @@ from . import services
from . import validators
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
class UserStoryViewSet(AssignedUsersSignalMixin, OCCResourceMixin,
VotedResourceMixin, HistoryResourceMixin,
WatchedResourceMixin, ByRefMixin, TaggedResourceMixin,
BlockedByProjectMixin, ModelCrudViewSet):
validator_class = validators.UserStoryValidator
queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,)

Some files were not shown because too many files have changed in this diff Show More