Compare commits
68 Commits
Author | SHA1 | Date |
---|---|---|
|
120442206b | |
|
d6d3f8c6a8 | |
|
3cebad87eb | |
|
f45f5ae08a | |
|
0434e8b78b | |
|
0c0e09819a | |
|
2b38fefa13 | |
|
7c5ba16d24 | |
|
b0d065167c | |
|
4bb12d73d9 | |
|
e736846562 | |
|
2bdd652ea7 | |
|
483d3ffd5f | |
|
2d77f8974b | |
|
77fa09a953 | |
|
df9830bb4f | |
|
849ce97a1c | |
|
c260a4dd22 | |
|
5f301450df | |
|
abf2b11220 | |
|
a4256c3f09 | |
|
18e97be27c | |
|
a17ed83755 | |
|
fe4cddac30 | |
|
0cb423c929 | |
|
39e9de71cf | |
|
4c74e6182f | |
|
dae83618a1 | |
|
97b69cdb61 | |
|
a5386cb79c | |
|
6a0a55f982 | |
|
2d91b75096 | |
|
0e1105fb2b | |
|
49520bcc9b | |
|
3cd0da80d5 | |
|
4fa4c02d16 | |
|
809a90b777 | |
|
305b8f048f | |
|
ad757f233b | |
|
8806fce1eb | |
|
7d8073f801 | |
|
90a12ee95a | |
|
10b5baefbb | |
|
80002086d5 | |
|
9b70d25f49 | |
|
8e4d1a5653 | |
|
a35db80932 | |
|
533d72fee6 | |
|
117fc011f7 | |
|
963763be31 | |
|
22eae180c6 | |
|
6842a7bb21 | |
|
03bf1f0a9a | |
|
cb24850466 | |
|
203e37d98b | |
|
b47a4d5dad | |
|
9fd40ce1bc | |
|
08e31a2ca9 | |
|
4fe58359f4 | |
|
4b5e58f806 | |
|
81ff7fd666 | |
|
fc9af7d1df | |
|
54fe5ba79c | |
|
5ac2cf6146 | |
|
2d7229e494 | |
|
f188974f31 | |
|
0512b1a47d | |
|
9389b65157 |
|
@ -17,8 +17,9 @@ before_install:
|
||||||
- sudo /etc/init.d/postgresql start 9.4
|
- sudo /etc/init.d/postgresql start 9.4
|
||||||
- psql -c 'create database taiga;' -U postgres
|
- psql -c 'create database taiga;' -U postgres
|
||||||
install:
|
install:
|
||||||
- travis_retry pip install -r requirements-devel.txt
|
- travis_retry pip install pipenv
|
||||||
|
- travis_retry pipenv sync --dev
|
||||||
script:
|
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:
|
after_success:
|
||||||
- coveralls
|
- coveralls
|
||||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -2,6 +2,60 @@
|
||||||
|
|
||||||
## Unreleased
|
## 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)
|
## 3.4.5 (2018-10-15)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
|
@ -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"
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,7 +96,7 @@ python manage.py loaddata initial_project_templates
|
||||||
python manage.py sample_data
|
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
|
Initial auth data: admin/123123
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE = settings.testing
|
DJANGO_SETTINGS_MODULE = settings.testing
|
||||||
python_paths = .
|
filterwarnings =
|
||||||
|
once
|
||||||
|
ignore::django.utils.deprecation.RemovedInDjango20Warning
|
||||||
|
ignore::DeprecationWarning:taiga.base.api.serializers
|
||||||
|
|
|
@ -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
|
|
|
@ -1,45 +1,63 @@
|
||||||
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
|
-i https://pypi.org/simple
|
||||||
CairoSVG==2.0.3
|
amqp==2.3.2
|
||||||
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
|
|
||||||
asana==0.6.7
|
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
|
celery==4.0.2
|
||||||
kombu==4.0.2
|
certifi==2018.11.29
|
||||||
cryptography==1.9
|
cffi==1.11.5
|
||||||
|
chardet==3.0.4
|
||||||
|
contextlib2==0.5.5
|
||||||
|
cryptography==2.3.1
|
||||||
|
cssselect==1.0.3
|
||||||
cssutils==1.0.2
|
cssutils==1.0.2
|
||||||
diff-match-patch==20121119
|
diff-match-patch==20121119
|
||||||
django-ipware==1.1.6
|
django-ipware==1.1.6
|
||||||
django-jinja==2.3.1
|
django-jinja==2.3.1
|
||||||
|
django-pglocks==1.0.2
|
||||||
django-picklefield==0.3.2
|
django-picklefield==0.3.2
|
||||||
django-sampledatahelper==0.4.1
|
django-sampledatahelper==0.4.1
|
||||||
django-sites==0.9
|
django-sites==0.9
|
||||||
django-sr==0.0.4
|
django-sr==0.0.4
|
||||||
|
django==1.11.16
|
||||||
djmail==1.0.1
|
djmail==1.0.1
|
||||||
|
docopt==0.6.2
|
||||||
easy-thumbnails==2.4.1
|
easy-thumbnails==2.4.1
|
||||||
fn==0.4.3
|
fn==0.4.3
|
||||||
git+https://github.com/Xof/django-pglocks.git
|
|
||||||
gunicorn==19.7.1
|
gunicorn==19.7.1
|
||||||
jinja2==2.9.6
|
html5lib==1.0.1
|
||||||
idna==2.5
|
idna==2.7
|
||||||
lxml==3.8.0
|
jinja2==2.10
|
||||||
|
kombu==4.0.2
|
||||||
|
lxml==4.2.5
|
||||||
|
markdown==3.0.1
|
||||||
|
markupsafe==1.1.0
|
||||||
netaddr==0.7.19
|
netaddr==0.7.19
|
||||||
|
oauthlib==2.1.0
|
||||||
|
olefile==0.46
|
||||||
|
pillow==4.1.1
|
||||||
premailer==3.0.1
|
premailer==3.0.1
|
||||||
psd-tools==1.4
|
psd-tools==1.4
|
||||||
psycopg2==2.7.4
|
psycopg2-binary==2.7.5
|
||||||
|
pycparser==2.19
|
||||||
pygments==2.2.0
|
pygments==2.2.0
|
||||||
pyjwkest==1.3.2
|
pyjwt==1.7.0
|
||||||
python-dateutil==2.6.0
|
python-dateutil==2.7.5
|
||||||
python-magic==0.4.13
|
python-magic==0.4.13
|
||||||
pytz==2017.2
|
pytz==2018.7
|
||||||
raven==6.1.0
|
raven==6.1.0
|
||||||
redis==2.10.5
|
redis==2.10.5
|
||||||
requests-oauthlib==0.8.0
|
requests-oauthlib==0.8.0
|
||||||
requests==2.17.3
|
requests==2.20.0
|
||||||
|
sampledata==0.3.7
|
||||||
serpy==0.1.1
|
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
|
webcolors==1.7
|
||||||
|
webencodings==0.5.1
|
||||||
|
|
|
@ -242,11 +242,7 @@ You need transifex-client, install it.
|
||||||
|
|
||||||
1. Install transifex-client, use
|
1. Install transifex-client, use
|
||||||
|
|
||||||
$ pip install --upgrade -r requirements-devel.txt
|
$ pip install --upgrade transifex-client
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
$ pip install --upgrade transifex-client==0.12.2
|
|
||||||
|
|
||||||
2. Create ~/.transifexrc file:
|
2. Create ~/.transifexrc file:
|
||||||
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
from .celery import *
|
|
||||||
|
|
||||||
# To use celery in memory
|
|
||||||
#task_always_eager = True
|
|
|
@ -148,7 +148,7 @@ LANGUAGES = [
|
||||||
("tr", "Türkçe"), # Turkish
|
("tr", "Türkçe"), # Turkish
|
||||||
#("tt", "татар теле"), # Tatar
|
#("tt", "татар теле"), # Tatar
|
||||||
#("udm", "удмурт кыл"), # Udmurt
|
#("udm", "удмурт кыл"), # Udmurt
|
||||||
#("uk", "Українська"), # Ukrainian
|
("uk", "Українська"), # Ukrainian
|
||||||
#("ur", "اردو"), # Urdu
|
#("ur", "اردو"), # Urdu
|
||||||
#("vi", "Tiếng Việt"), # Vietnamese
|
#("vi", "Tiếng Việt"), # Vietnamese
|
||||||
("zh-hans", "中文(简体)"), # Simplified Chinese
|
("zh-hans", "中文(简体)"), # Simplified Chinese
|
||||||
|
@ -309,6 +309,7 @@ INSTALLED_APPS = [
|
||||||
"taiga.projects.issues",
|
"taiga.projects.issues",
|
||||||
"taiga.projects.wiki",
|
"taiga.projects.wiki",
|
||||||
"taiga.projects.contact",
|
"taiga.projects.contact",
|
||||||
|
"taiga.projects.settings",
|
||||||
"taiga.searches",
|
"taiga.searches",
|
||||||
"taiga.timeline",
|
"taiga.timeline",
|
||||||
"taiga.mdrender",
|
"taiga.mdrender",
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
ignore = E41,E266
|
ignore = E41,E266
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude =
|
exclude =
|
||||||
|
.cache,
|
||||||
.git,
|
.git,
|
||||||
|
.tox,
|
||||||
|
.venv,
|
||||||
*__pycache__*,
|
*__pycache__*,
|
||||||
*tests*,
|
*tests*,
|
||||||
*scripts*,
|
*scripts*,
|
||||||
|
|
|
@ -375,14 +375,18 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi
|
||||||
class BaseRelatedFieldsFilter(FilterBackend):
|
class BaseRelatedFieldsFilter(FilterBackend):
|
||||||
filter_name = None
|
filter_name = None
|
||||||
param_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:
|
if filter_name:
|
||||||
self.filter_name = filter_name
|
self.filter_name = filter_name
|
||||||
|
|
||||||
if param_name:
|
if param_name:
|
||||||
self.param_name = param_name
|
self.param_name = param_name
|
||||||
|
|
||||||
|
if exclude_param_name:
|
||||||
|
self.exclude_param_name
|
||||||
|
|
||||||
def _prepare_filter_data(self, query_param_value):
|
def _prepare_filter_data(self, query_param_value):
|
||||||
def _transform_value(value):
|
def _transform_value(value):
|
||||||
try:
|
try:
|
||||||
|
@ -396,48 +400,57 @@ class BaseRelatedFieldsFilter(FilterBackend):
|
||||||
values = map(_transform_value, values)
|
values = map(_transform_value, values)
|
||||||
return list(values)
|
return list(values)
|
||||||
|
|
||||||
def _get_queryparams(self, params):
|
def _get_queryparams(self, params, mode=''):
|
||||||
param_name = self.param_name or self.filter_name
|
param_name = self.exclude_param_name if mode == 'exclude' else self.param_name or self.filter_name
|
||||||
raw_value = params.get(param_name, None)
|
raw_value = params.get(param_name, None)
|
||||||
|
|
||||||
if raw_value:
|
if raw_value:
|
||||||
value = self._prepare_filter_data(raw_value)
|
value = self._prepare_filter_data(raw_value)
|
||||||
|
|
||||||
if None in value:
|
if None in value:
|
||||||
qs_in_kwargs = {"{}__in".format(self.filter_name): [v for v in value if v is not None]}
|
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}
|
qs_isnull_kwargs = {"{}__isnull".format(self.filter_name): True}
|
||||||
return Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs)
|
return Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs)
|
||||||
else:
|
else:
|
||||||
return {"{}__in".format(self.filter_name): value}
|
return Q(**{"{}__in".format(self.filter_name): value})
|
||||||
|
|
||||||
return None
|
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):
|
def filter_queryset(self, request, queryset, view):
|
||||||
query = self._get_queryparams(request.QUERY_PARAMS)
|
operations = {
|
||||||
if query:
|
"filter": self._prepare_filter_query,
|
||||||
if isinstance(query, dict):
|
"exclude": self._prepare_exclude_query,
|
||||||
queryset = queryset.filter(**query)
|
}
|
||||||
else:
|
|
||||||
queryset = queryset.filter(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)
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
|
||||||
class OwnersFilter(BaseRelatedFieldsFilter):
|
class OwnersFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = 'owner'
|
filter_name = 'owner'
|
||||||
|
exclude_param_name = 'exclude_owner'
|
||||||
|
|
||||||
|
|
||||||
class AssignedToFilter(BaseRelatedFieldsFilter):
|
class AssignedToFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = 'assigned_to'
|
filter_name = 'assigned_to'
|
||||||
|
exclude_param_name = 'exclude_assigned_to'
|
||||||
|
|
||||||
|
|
||||||
class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
|
class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
|
||||||
filter_name = 'assigned_users'
|
filter_name = 'assigned_users'
|
||||||
|
exclude_param_name = 'exclude_assigned_users'
|
||||||
|
|
||||||
def _get_queryparams(self, params):
|
def _get_queryparams(self, params, mode=''):
|
||||||
param_name = self.param_name or self.filter_name
|
param_name = self.exclude_param_name if mode == 'exclude' else self.param_name or self.filter_name
|
||||||
raw_value = params.get(param_name, None)
|
raw_value = params.get(param_name, None)
|
||||||
|
|
||||||
if raw_value:
|
if raw_value:
|
||||||
value = self._prepare_filter_data(raw_value)
|
value = self._prepare_filter_data(raw_value)
|
||||||
UserStoryModel = apps.get_model("userstories", "UserStory")
|
UserStoryModel = apps.get_model("userstories", "UserStory")
|
||||||
|
@ -461,38 +474,65 @@ class AssignedUsersFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
|
||||||
|
|
||||||
class StatusesFilter(BaseRelatedFieldsFilter):
|
class StatusesFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = 'status'
|
filter_name = 'status'
|
||||||
|
exclude_param_name = 'exclude_status'
|
||||||
|
|
||||||
|
|
||||||
class IssueTypesFilter(BaseRelatedFieldsFilter):
|
class IssueTypesFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = 'type'
|
filter_name = 'type'
|
||||||
|
param_name = 'type'
|
||||||
|
exclude_param_name = 'exclude_type'
|
||||||
|
|
||||||
|
|
||||||
class PrioritiesFilter(BaseRelatedFieldsFilter):
|
class PrioritiesFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = 'priority'
|
filter_name = 'priority'
|
||||||
|
exclude_param_name = 'exclude_priority'
|
||||||
|
|
||||||
|
|
||||||
class SeveritiesFilter(BaseRelatedFieldsFilter):
|
class SeveritiesFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = 'severity'
|
filter_name = 'severity'
|
||||||
|
exclude_param_name = 'exclude_severity'
|
||||||
|
|
||||||
|
|
||||||
class TagsFilter(FilterBackend):
|
class TagsFilter(FilterBackend):
|
||||||
filter_name = 'tags'
|
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:
|
if filter_name:
|
||||||
self.filter_name = filter_name
|
self.filter_name = filter_name
|
||||||
|
|
||||||
def _get_tags_queryparams(self, params):
|
if exclude_param_name:
|
||||||
tags = params.get(self.filter_name, None)
|
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:
|
if tags:
|
||||||
return tags.split(",")
|
return tags.split(",")
|
||||||
|
|
||||||
return None
|
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):
|
def filter_queryset(self, request, queryset, view):
|
||||||
query_tags = self._get_tags_queryparams(request.QUERY_PARAMS)
|
operations = {
|
||||||
if query_tags:
|
"filter": self._prepare_filter_query,
|
||||||
queryset = queryset.filter(tags__contains=query_tags)
|
"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)
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
@ -631,18 +671,22 @@ class QFilter(FilterBackend):
|
||||||
class RoleFilter(BaseRelatedFieldsFilter):
|
class RoleFilter(BaseRelatedFieldsFilter):
|
||||||
filter_name = "role_id"
|
filter_name = "role_id"
|
||||||
param_name = "role"
|
param_name = "role"
|
||||||
|
exclude_param_name = "exclude_role"
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
Membership = apps.get_model('projects', 'Membership')
|
Membership = apps.get_model('projects', 'Membership')
|
||||||
query = self._get_queryparams(request.QUERY_PARAMS)
|
|
||||||
if query:
|
operations = {
|
||||||
if isinstance(query, dict):
|
"filter": self._prepare_filter_query,
|
||||||
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
|
"exclude": self._prepare_exclude_query,
|
||||||
queryset = queryset.filter(assigned_to__in=memberships)
|
}
|
||||||
else:
|
|
||||||
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
|
for mode, qs_method in operations.items():
|
||||||
if memberships:
|
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
|
||||||
queryset = queryset.filter(assigned_to__in=memberships)
|
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)
|
return FilterBackend.filter_queryset(self, request, queryset, view)
|
||||||
|
|
||||||
|
@ -650,20 +694,24 @@ class RoleFilter(BaseRelatedFieldsFilter):
|
||||||
class UserStoriesRoleFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
|
class UserStoriesRoleFilter(FilterModelAssignedUsers, BaseRelatedFieldsFilter):
|
||||||
filter_name = "role_id"
|
filter_name = "role_id"
|
||||||
param_name = "role"
|
param_name = "role"
|
||||||
|
exclude_param_name = 'exclude_role'
|
||||||
|
|
||||||
def filter_queryset(self, request, queryset, view):
|
def filter_queryset(self, request, queryset, view):
|
||||||
Membership = apps.get_model('projects', 'Membership')
|
Membership = apps.get_model('projects', 'Membership')
|
||||||
query = self._get_queryparams(request.QUERY_PARAMS)
|
|
||||||
|
|
||||||
if query:
|
operations = {
|
||||||
if isinstance(query, dict):
|
"filter": self._prepare_filter_query,
|
||||||
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
|
"exclude": self._prepare_exclude_query,
|
||||||
else:
|
}
|
||||||
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
|
|
||||||
if memberships:
|
for mode, qs_method in operations.items():
|
||||||
user_story_model = apps.get_model("userstories", "UserStory")
|
query = self._get_queryparams(request.QUERY_PARAMS, mode=mode)
|
||||||
queryset = queryset.filter(
|
if query:
|
||||||
self.get_assigned_users_filter(user_story_model, memberships)
|
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)
|
return FilterBackend.filter_queryset(self, request, queryset, view)
|
||||||
|
|
|
@ -91,6 +91,22 @@ def emit_event_for_model(obj, *, type:str="change", channel:str="events",
|
||||||
sessionid=sessionid,
|
sessionid=sessionid,
|
||||||
data=data)
|
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",
|
def emit_live_notification_for_model(obj, user, history, *, type:str="change", channel:str="events",
|
||||||
sessionid:str="not-existing"):
|
sessionid:str="not-existing"):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
from taiga.users.models import User
|
from taiga.users.models import User
|
||||||
from taiga.projects.models import Membership
|
from taiga.projects.models import Membership
|
||||||
|
|
||||||
|
from taiga.permissions.choices import ANON_PERMISSIONS
|
||||||
|
|
||||||
|
|
||||||
def resolve_users_bindings(users_bindings):
|
def resolve_users_bindings(users_bindings):
|
||||||
new_users_bindings = {}
|
new_users_bindings = {}
|
||||||
|
@ -50,3 +52,15 @@ def create_memberships(users_bindings, project, creator, role_name):
|
||||||
is_admin=False,
|
is_admin=False,
|
||||||
invited_by=creator,
|
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()
|
||||||
|
|
|
@ -262,6 +262,7 @@ class TrelloImporter:
|
||||||
project=project
|
project=project
|
||||||
)
|
)
|
||||||
import_service.create_memberships(options.get('users_bindings', {}), project, self._user, "trello")
|
import_service.create_memberships(options.get('users_bindings', {}), project, self._user, "trello")
|
||||||
|
import_service.set_base_permissions_for_project(project)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
def _import_user_stories_data(self, data, project, options):
|
def _import_user_stories_data(self, data, project, options):
|
||||||
|
|
|
@ -27,8 +27,8 @@ msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
||||||
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
|
"PO-Revision-Date: 2018-11-14 12:59+0000\n"
|
||||||
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
|
"Last-Translator: Hans Raaf\n"
|
||||||
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
|
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
|
||||||
"language/de/)\n"
|
"language/de/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -557,7 +557,7 @@ msgstr ""
|
||||||
|
|
||||||
#: taiga/events/events.py:106
|
#: taiga/events/events.py:106
|
||||||
msgid "User story created"
|
msgid "User story created"
|
||||||
msgstr "User story erstellt"
|
msgstr "User-Story erstellt"
|
||||||
|
|
||||||
#: taiga/events/events.py:109
|
#: taiga/events/events.py:109
|
||||||
msgid "User story changed"
|
msgid "User story changed"
|
||||||
|
@ -2841,8 +2841,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Neues Ticket wurde erstellt</h1>\n"
|
"<h1>Neues Ticket wurde erstellt</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat ein neues Ticket erstellt in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat ein neues Ticket in "
|
||||||
"%(project)s</p>\n"
|
"%(project)s erstellt</p>\n"
|
||||||
"<p>Ticket #%(ref)s %(subject)s</p>\n"
|
"<p>Ticket #%(ref)s %(subject)s</p>\n"
|
||||||
"<a class=\"button\" href=\"%(url)s\" title=\"See Issue #%(ref)s %(subject)s"
|
"<a class=\"button\" href=\"%(url)s\" title=\"See Issue #%(ref)s %(subject)s"
|
||||||
"\">Ticket ansehen</a>\n"
|
"\">Ticket ansehen</a>\n"
|
||||||
|
@ -2862,7 +2862,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Neues Ticket wurde erstellt\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"
|
"Ticket ansehen #%(ref)s %(subject)s auf %(url)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
|
@ -2988,8 +2988,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Neuer Sprint wurde erstellt</h1>\n"
|
"<h1>Neuer Sprint wurde erstellt</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat einen neuen Sprint erstellt in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat einen neuen Sprint in "
|
||||||
"%(project)s</p>\n"
|
"%(project)s erstellt</p>\n"
|
||||||
"<p>Sprint %(name)s</p>\n"
|
"<p>Sprint %(name)s</p>\n"
|
||||||
"<a class=\"button\" href=\"%(url)s\" title=\"See Sprint %(name)s\">See "
|
"<a class=\"button\" href=\"%(url)s\" title=\"See Sprint %(name)s\">See "
|
||||||
"sprint</a>\n"
|
"sprint</a>\n"
|
||||||
|
@ -3009,7 +3009,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Neuer Sprint wurde erstellt\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"
|
"Sprint ansehen %(name)s at %(url)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
|
@ -3058,7 +3058,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Sprint wurde gelöscht\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"
|
"Sprint %(name)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
|
@ -3135,8 +3135,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Neue Aufgabe wurde erstellt</h1>\n"
|
"<h1>Neue Aufgabe wurde erstellt</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Aufgabe erstellt in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Aufgabe in "
|
||||||
"%(project)s</p>\n"
|
"%(project)s erstellt</p>\n"
|
||||||
"<p>Aufgabe #%(ref)s %(subject)s</p>\n"
|
"<p>Aufgabe #%(ref)s %(subject)s</p>\n"
|
||||||
"<a class=\"button\" href=\"%(url)s\" title=\"See Task #%(ref)s %(subject)s"
|
"<a class=\"button\" href=\"%(url)s\" title=\"See Task #%(ref)s %(subject)s"
|
||||||
"\">Aufgabe ansehen</a>\n"
|
"\">Aufgabe ansehen</a>\n"
|
||||||
|
@ -3156,7 +3156,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Neue Aufgabe wurde erstellt\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"
|
"Aufgabe ansehen #%(ref)s %(subject)s auf %(url)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -3187,8 +3187,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Aufgabe wurde gelöscht</h1>\n"
|
"<h1>Aufgabe wurde gelöscht</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat eine Aufgabe gelöscht in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat eine Aufgabe in %(project)s "
|
||||||
"%(project)s</p>\n"
|
"gelöscht</p>\n"
|
||||||
"<p>Aufgabe #%(ref)s %(subject)s</p>\n"
|
"<p>Aufgabe #%(ref)s %(subject)s</p>\n"
|
||||||
"<p><small>Das Taiga Team</small></p>\n"
|
"<p><small>Das Taiga Team</small></p>\n"
|
||||||
" "
|
" "
|
||||||
|
@ -3206,7 +3206,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Aufgabe wurde gelöscht\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"
|
"Aufgabe #%(ref)s %(subject)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
|
@ -3283,8 +3283,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Neue User-Story wurde erstellt</h1>\n"
|
"<h1>Neue User-Story wurde erstellt</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue User-Story erstellt in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue User-Story in "
|
||||||
"%(project)s</p>\n"
|
"%(project)s erstellt</p>\n"
|
||||||
"<p>User-Story #%(ref)s %(subject)s</p>\n"
|
"<p>User-Story #%(ref)s %(subject)s</p>\n"
|
||||||
"<a class=\"button\" href=\"%(url)s\" title=\"See User Story #%(ref)s "
|
"<a class=\"button\" href=\"%(url)s\" title=\"See User Story #%(ref)s "
|
||||||
"%(subject)s\">User-Story ansehen</a>\n"
|
"%(subject)s\">User-Story ansehen</a>\n"
|
||||||
|
@ -3305,8 +3305,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Neue User-Story wurde erstellt\n"
|
"Neue User-Story wurde erstellt\n"
|
||||||
"Hallo %(user)s, %(changer)s hat eine neue User-Story erstellt in "
|
"Hallo %(user)s, %(changer)s hat eine neue User-Story in %(project)s "
|
||||||
"%(project)s\n"
|
"erstellt\n"
|
||||||
"User-Story ansehen #%(ref)s %(subject)s auf %(url)s\n"
|
"User-Story ansehen #%(ref)s %(subject)s auf %(url)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
|
@ -3336,8 +3336,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>User-Story wurde gelöscht</h1>\n"
|
"<h1>User-Story wurde gelöscht</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat eine User-Story gelöscht in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat eine User-Story in %(project)s "
|
||||||
"%(project)s</p>\n"
|
"gelöscht</p>\n"
|
||||||
"<p>User-Story #%(ref)s %(subject)s</p>\n"
|
"<p>User-Story #%(ref)s %(subject)s</p>\n"
|
||||||
"<p><small>Das Taiga Team</small></p>\n"
|
"<p><small>Das Taiga Team</small></p>\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -3356,7 +3356,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"User-Story wurde gelöscht\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"
|
"User-Story #%(ref)s %(subject)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
"---\n"
|
"---\n"
|
||||||
|
@ -3438,8 +3438,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Neue Wiki Seite wurde erstellt</h1>\n"
|
"<h1>Neue Wiki Seite wurde erstellt</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Wiki Seite erstellt in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat eine neue Wiki Seite in "
|
||||||
"%(project)s</p>\n"
|
"%(project)s erstellt</p>\n"
|
||||||
"<p>Wiki Seite %(page)s</p>\n"
|
"<p>Wiki Seite %(page)s</p>\n"
|
||||||
"<a class=\"button\" href=\"%(url)s\" title=\"Wiki page %(page)s\">Wiki Seite "
|
"<a class=\"button\" href=\"%(url)s\" title=\"Wiki page %(page)s\">Wiki Seite "
|
||||||
"ansehen</a>\n"
|
"ansehen</a>\n"
|
||||||
|
@ -3463,8 +3463,8 @@ msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Neue Wiki Seite wurde erstellt\n"
|
"Neue Wiki Seite wurde erstellt\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Hallo %(user)s, %(changer)s hat eine neue Wiki Seite erstellt in "
|
"Hallo %(user)s, %(changer)s hat eine neue Wiki Seite in %(project)s "
|
||||||
"%(project)s\n"
|
"erstellt\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Wiki Seite ansehen %(page)s auf %(url)s\n"
|
"Wiki Seite ansehen %(page)s auf %(url)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -3495,8 +3495,8 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"<h1>Wiki Seite wurde gelöscht</h1>\n"
|
"<h1>Wiki Seite wurde gelöscht</h1>\n"
|
||||||
"<p>Hallo %(user)s,<br />%(changer)s hat eine Wiki Seite gelöscht in "
|
"<p>Hallo %(user)s,<br />%(changer)s hat eine Wiki Seite in "
|
||||||
"%(project)s</p>\n"
|
"%(project)s gelöscht</p>\n"
|
||||||
"<p>Wiki Seite %(page)s</p>\n"
|
"<p>Wiki Seite %(page)s</p>\n"
|
||||||
"<p><small>Das Taiga Team</small></p>\n"
|
"<p><small>Das Taiga Team</small></p>\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
@ -3518,7 +3518,7 @@ msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
"Wiki Seite wurde gelöscht\n"
|
"Wiki Seite wurde gelöscht\n"
|
||||||
"\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"
|
"\n"
|
||||||
"Wiki Seite %(page)s\n"
|
"Wiki Seite %(page)s\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
|
|
@ -4,14 +4,16 @@
|
||||||
#
|
#
|
||||||
# Translators:
|
# Translators:
|
||||||
# Translators:
|
# Translators:
|
||||||
|
# Amirhoshang Hoseinpour Dehkordi <amir.hoseinpour@gmail.com>, 2018
|
||||||
# Vahid Dayyani <vahid.dayyani@ymail.com>, 2018
|
# Vahid Dayyani <vahid.dayyani@ymail.com>, 2018
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
||||||
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
|
"PO-Revision-Date: 2018-12-02 10:54+0000\n"
|
||||||
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
|
"Last-Translator: Amirhoshang Hoseinpour Dehkordi <amir.hoseinpour@gmail."
|
||||||
|
"com>\n"
|
||||||
"Language-Team: Persian (Iran) (http://www.transifex.com/taiga-agile-llc/"
|
"Language-Team: Persian (Iran) (http://www.transifex.com/taiga-agile-llc/"
|
||||||
"taiga-back/language/fa_IR/)\n"
|
"taiga-back/language/fa_IR/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -26,7 +28,7 @@ msgstr "ثبت نام عمومی غیرفعال است."
|
||||||
|
|
||||||
#: taiga/auth/api.py:93
|
#: taiga/auth/api.py:93
|
||||||
msgid "You must accept our terms of service and privacy policy"
|
msgid "You must accept our terms of service and privacy policy"
|
||||||
msgstr ""
|
msgstr "شما باید موارد سرویس و سیاست های امنیت ما را قبول کنید."
|
||||||
|
|
||||||
#: taiga/auth/api.py:102
|
#: taiga/auth/api.py:102
|
||||||
msgid "invalid register type"
|
msgid "invalid register type"
|
||||||
|
@ -73,7 +75,7 @@ msgstr "نام کاربری نامعتبر"
|
||||||
#: taiga/auth/validators.py:42 taiga/users/validators.py:50
|
#: taiga/auth/validators.py:42 taiga/users/validators.py:50
|
||||||
msgid ""
|
msgid ""
|
||||||
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
|
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
|
||||||
msgstr "ضروری است. ۲۵۵ کاراکتر یا کمتر. حروف و اعداد و . و - و ـ مجاز است."
|
msgstr "255 کاراکتر یا کمتر ضروری است. حروف و اعداد و . و - و ـ مجاز است."
|
||||||
|
|
||||||
#: taiga/base/api/fields.py:294
|
#: taiga/base/api/fields.py:294
|
||||||
msgid "This field is required."
|
msgid "This field is required."
|
||||||
|
@ -86,7 +88,7 @@ msgstr "مقدار نامعتبر."
|
||||||
#: taiga/base/api/fields.py:484
|
#: taiga/base/api/fields.py:484
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' value must be either True or False."
|
msgid "'%s' value must be either True or False."
|
||||||
msgstr "'%s' میبایست True (صحیح) یا False (غلط) باشد."
|
msgstr "'%s' میبایست صحیح یا غلط باشد."
|
||||||
|
|
||||||
#: taiga/base/api/fields.py:549
|
#: taiga/base/api/fields.py:549
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -517,91 +519,91 @@ msgstr ""
|
||||||
|
|
||||||
#: taiga/base/utils/urls.py:68
|
#: taiga/base/utils/urls.py:68
|
||||||
msgid "Host access error"
|
msgid "Host access error"
|
||||||
msgstr ""
|
msgstr "خطای دسترسی به هاست"
|
||||||
|
|
||||||
#: taiga/base/utils/urls.py:74
|
#: taiga/base/utils/urls.py:74
|
||||||
msgid "IP Address error"
|
msgid "IP Address error"
|
||||||
msgstr ""
|
msgstr "خطای آدرس آی پی"
|
||||||
|
|
||||||
#: taiga/events/events.py:106
|
#: taiga/events/events.py:106
|
||||||
msgid "User story created"
|
msgid "User story created"
|
||||||
msgstr ""
|
msgstr "گزارش کاربر ساخته شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:109
|
#: taiga/events/events.py:109
|
||||||
msgid "User story changed"
|
msgid "User story changed"
|
||||||
msgstr ""
|
msgstr "گزارش کاربر تغییر کرد."
|
||||||
|
|
||||||
#: taiga/events/events.py:112
|
#: taiga/events/events.py:112
|
||||||
msgid "User story deleted"
|
msgid "User story deleted"
|
||||||
msgstr ""
|
msgstr "گزارش کاربر حذف شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:114
|
#: taiga/events/events.py:114
|
||||||
msgid "US #{} - {}"
|
msgid "US #{} - {}"
|
||||||
msgstr ""
|
msgstr "US #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:117
|
#: taiga/events/events.py:117
|
||||||
msgid "Task created"
|
msgid "Task created"
|
||||||
msgstr ""
|
msgstr "وظیفه ساخته شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:120
|
#: taiga/events/events.py:120
|
||||||
msgid "Task changed"
|
msgid "Task changed"
|
||||||
msgstr ""
|
msgstr "وظیفه تغییر کرد."
|
||||||
|
|
||||||
#: taiga/events/events.py:123
|
#: taiga/events/events.py:123
|
||||||
msgid "Task deleted"
|
msgid "Task deleted"
|
||||||
msgstr ""
|
msgstr "وظیفه حذف شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:125
|
#: taiga/events/events.py:125
|
||||||
msgid "Task #{} - {}"
|
msgid "Task #{} - {}"
|
||||||
msgstr ""
|
msgstr "وظیفه #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:128
|
#: taiga/events/events.py:128
|
||||||
msgid "Issue created"
|
msgid "Issue created"
|
||||||
msgstr ""
|
msgstr "موضوع ساخته شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:131
|
#: taiga/events/events.py:131
|
||||||
msgid "Issue changed"
|
msgid "Issue changed"
|
||||||
msgstr ""
|
msgstr "موضوع تغییر کرد."
|
||||||
|
|
||||||
#: taiga/events/events.py:134
|
#: taiga/events/events.py:134
|
||||||
msgid "Issue deleted"
|
msgid "Issue deleted"
|
||||||
msgstr ""
|
msgstr "موضوع حذف شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:136
|
#: taiga/events/events.py:136
|
||||||
msgid "Issue: #{} - {}"
|
msgid "Issue: #{} - {}"
|
||||||
msgstr ""
|
msgstr "موضوع: #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:139
|
#: taiga/events/events.py:139
|
||||||
msgid "Wiki Page created"
|
msgid "Wiki Page created"
|
||||||
msgstr ""
|
msgstr "صفحه ویکی ساخته شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:142
|
#: taiga/events/events.py:142
|
||||||
msgid "Wiki Page changed"
|
msgid "Wiki Page changed"
|
||||||
msgstr ""
|
msgstr "صفحه ویکی تغییر داده شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:145
|
#: taiga/events/events.py:145
|
||||||
msgid "Wiki Page deleted"
|
msgid "Wiki Page deleted"
|
||||||
msgstr ""
|
msgstr "صفحه ویکی حذف شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:147
|
#: taiga/events/events.py:147
|
||||||
msgid "Wiki Page: {}"
|
msgid "Wiki Page: {}"
|
||||||
msgstr ""
|
msgstr "صفحه ویکی: {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:150
|
#: taiga/events/events.py:150
|
||||||
msgid "Sprint created"
|
msgid "Sprint created"
|
||||||
msgstr ""
|
msgstr "سرعتی ساخته شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:153
|
#: taiga/events/events.py:153
|
||||||
msgid "Sprint changed"
|
msgid "Sprint changed"
|
||||||
msgstr ""
|
msgstr "سرعتی تغییر داده شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:156
|
#: taiga/events/events.py:156
|
||||||
msgid "Sprint deleted"
|
msgid "Sprint deleted"
|
||||||
msgstr ""
|
msgstr "سرعتی حذف شد."
|
||||||
|
|
||||||
#: taiga/events/events.py:158
|
#: taiga/events/events.py:158
|
||||||
msgid "Sprint: {}"
|
msgid "Sprint: {}"
|
||||||
msgstr ""
|
msgstr "سرعتی: {}"
|
||||||
|
|
||||||
#: taiga/export_import/api.py:127
|
#: taiga/export_import/api.py:127
|
||||||
msgid "We needed at least one role"
|
msgid "We needed at least one role"
|
||||||
|
@ -1782,27 +1784,27 @@ msgstr "کاربر میبایست قبلاً از اعضای پروژه بو
|
||||||
|
|
||||||
#: taiga/projects/api.py:642
|
#: taiga/projects/api.py:642
|
||||||
msgid "You can't delete user story due date by default"
|
msgid "You can't delete user story due date by default"
|
||||||
msgstr ""
|
msgstr "شما سر رسید گزارش کاربر را نمی توانید بصورت عادی حذف کنید."
|
||||||
|
|
||||||
#: taiga/projects/api.py:658
|
#: taiga/projects/api.py:658
|
||||||
msgid "Project already have due dates"
|
msgid "Project already have due dates"
|
||||||
msgstr ""
|
msgstr "پروژه موعد سر رسید دارد."
|
||||||
|
|
||||||
#: taiga/projects/api.py:718
|
#: taiga/projects/api.py:718
|
||||||
msgid "You can't delete task due date by default"
|
msgid "You can't delete task due date by default"
|
||||||
msgstr ""
|
msgstr "شما سر رسید وظیفه کاربر را نمی توانید بصورت عادی حذف کنید."
|
||||||
|
|
||||||
#: taiga/projects/api.py:734
|
#: taiga/projects/api.py:734
|
||||||
msgid "Project already have task due dates"
|
msgid "Project already have task due dates"
|
||||||
msgstr ""
|
msgstr "پروژه موعد سر رسید وظیفه دارد."
|
||||||
|
|
||||||
#: taiga/projects/api.py:858
|
#: taiga/projects/api.py:858
|
||||||
msgid "You can't delete issue due date by default"
|
msgid "You can't delete issue due date by default"
|
||||||
msgstr ""
|
msgstr "شما سر رسید موضوع کاربر را نمی توانید بصورت عادی حذف کنید."
|
||||||
|
|
||||||
#: taiga/projects/api.py:874
|
#: taiga/projects/api.py:874
|
||||||
msgid "Project already have issue due dates"
|
msgid "Project already have issue due dates"
|
||||||
msgstr ""
|
msgstr "پروژه موعد سر رسید موضوع دارد."
|
||||||
|
|
||||||
#: taiga/projects/api.py:1023
|
#: taiga/projects/api.py:1023
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -2051,11 +2053,11 @@ msgstr "موردی با همین نام وجود دارد."
|
||||||
|
|
||||||
#: taiga/projects/due_dates/models.py:21
|
#: taiga/projects/due_dates/models.py:21
|
||||||
msgid "due date"
|
msgid "due date"
|
||||||
msgstr ""
|
msgstr "موعد سر رسید"
|
||||||
|
|
||||||
#: taiga/projects/due_dates/models.py:24
|
#: taiga/projects/due_dates/models.py:24
|
||||||
msgid "reason for the due date"
|
msgid "reason for the due date"
|
||||||
msgstr ""
|
msgstr "دلیل موعد سر رسید."
|
||||||
|
|
||||||
#: taiga/projects/epics/api.py:94
|
#: taiga/projects/epics/api.py:94
|
||||||
msgid "You don't have permissions to set this status to this epic."
|
msgid "You don't have permissions to set this status to this epic."
|
||||||
|
@ -2212,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:164
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:175
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:175
|
||||||
msgid "Not set"
|
msgid "Not set"
|
||||||
msgstr ""
|
msgstr "تنظیم نشده"
|
||||||
|
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:286
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:286
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:91
|
#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:91
|
||||||
|
@ -2567,12 +2569,12 @@ msgstr "ارزش"
|
||||||
#: taiga/projects/models.py:614 taiga/projects/models.py:671
|
#: taiga/projects/models.py:614 taiga/projects/models.py:671
|
||||||
#: taiga/projects/models.py:789
|
#: taiga/projects/models.py:789
|
||||||
msgid "by default"
|
msgid "by default"
|
||||||
msgstr ""
|
msgstr "بصورت عمومی"
|
||||||
|
|
||||||
#: taiga/projects/models.py:618 taiga/projects/models.py:675
|
#: taiga/projects/models.py:618 taiga/projects/models.py:675
|
||||||
#: taiga/projects/models.py:793
|
#: taiga/projects/models.py:793
|
||||||
msgid "days to due"
|
msgid "days to due"
|
||||||
msgstr ""
|
msgstr "روز تا موعد"
|
||||||
|
|
||||||
#: taiga/projects/models.py:823
|
#: taiga/projects/models.py:823
|
||||||
msgid "default owner's role"
|
msgid "default owner's role"
|
||||||
|
@ -2592,7 +2594,7 @@ msgstr "وضعیتهای استوریهای کاربری"
|
||||||
|
|
||||||
#: taiga/projects/models.py:849
|
#: taiga/projects/models.py:849
|
||||||
msgid "us duedates"
|
msgid "us duedates"
|
||||||
msgstr ""
|
msgstr "موعد ما"
|
||||||
|
|
||||||
#: taiga/projects/models.py:850 taiga/projects/userstories/models.py:45
|
#: taiga/projects/models.py:850 taiga/projects/userstories/models.py:45
|
||||||
#: taiga/projects/userstories/models.py:78
|
#: taiga/projects/userstories/models.py:78
|
||||||
|
@ -2605,7 +2607,7 @@ msgstr "وضعیتهای وظایف"
|
||||||
|
|
||||||
#: taiga/projects/models.py:852
|
#: taiga/projects/models.py:852
|
||||||
msgid "task duedates"
|
msgid "task duedates"
|
||||||
msgstr ""
|
msgstr "موعد وظیفه ها"
|
||||||
|
|
||||||
#: taiga/projects/models.py:853
|
#: taiga/projects/models.py:853
|
||||||
msgid "issue statuses"
|
msgid "issue statuses"
|
||||||
|
@ -2617,7 +2619,7 @@ msgstr "انواع موضوعات"
|
||||||
|
|
||||||
#: taiga/projects/models.py:855
|
#: taiga/projects/models.py:855
|
||||||
msgid "issue duedates"
|
msgid "issue duedates"
|
||||||
msgstr ""
|
msgstr "موعد موضوعات"
|
||||||
|
|
||||||
#: taiga/projects/models.py:856
|
#: taiga/projects/models.py:856
|
||||||
msgid "priorities"
|
msgid "priorities"
|
||||||
|
@ -4221,7 +4223,7 @@ msgstr "؟"
|
||||||
#. Translators: User story point value (value = 0)
|
#. Translators: User story point value (value = 0)
|
||||||
#: taiga/projects/translations.py:47
|
#: taiga/projects/translations.py:47
|
||||||
msgid "0"
|
msgid "0"
|
||||||
msgstr "۰"
|
msgstr "0"
|
||||||
|
|
||||||
#. Translators: User story point value (value = 0.5)
|
#. Translators: User story point value (value = 0.5)
|
||||||
#: taiga/projects/translations.py:49
|
#: taiga/projects/translations.py:49
|
||||||
|
@ -4461,7 +4463,7 @@ msgstr "تاریخ تکمیل"
|
||||||
|
|
||||||
#: taiga/projects/userstories/models.py:102
|
#: taiga/projects/userstories/models.py:102
|
||||||
msgid "assigned users"
|
msgid "assigned users"
|
||||||
msgstr ""
|
msgstr "کاربران متصل شده"
|
||||||
|
|
||||||
#: taiga/projects/userstories/models.py:111
|
#: taiga/projects/userstories/models.py:111
|
||||||
msgid "generated from issue"
|
msgid "generated from issue"
|
||||||
|
@ -4724,11 +4726,11 @@ msgstr "تاریخ عضویت"
|
||||||
|
|
||||||
#: taiga/users/models.py:155
|
#: taiga/users/models.py:155
|
||||||
msgid "accepted terms"
|
msgid "accepted terms"
|
||||||
msgstr ""
|
msgstr "قبول شرایط"
|
||||||
|
|
||||||
#: taiga/users/models.py:156
|
#: taiga/users/models.py:156
|
||||||
msgid "new terms read"
|
msgid "new terms read"
|
||||||
msgstr ""
|
msgstr "خواندن شرایط جدید"
|
||||||
|
|
||||||
#: taiga/users/models.py:158
|
#: taiga/users/models.py:158
|
||||||
msgid "default language"
|
msgid "default language"
|
||||||
|
@ -4972,7 +4974,7 @@ msgstr "نام کاربری نامعتبر. نام کاربری دیگری ان
|
||||||
|
|
||||||
#: taiga/users/validators.py:73
|
#: taiga/users/validators.py:73
|
||||||
msgid "Read new terms has to be true'"
|
msgid "Read new terms has to be true'"
|
||||||
msgstr ""
|
msgstr "خواندن شرایط جدید باید صحیح باشد"
|
||||||
|
|
||||||
#: taiga/userstorage/api.py:53
|
#: taiga/userstorage/api.py:53
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -5017,4 +5019,4 @@ msgstr "مدت زمان"
|
||||||
|
|
||||||
#: taiga/webhooks/validators.py:42
|
#: taiga/webhooks/validators.py:42
|
||||||
msgid "Not allowed IP Address"
|
msgid "Not allowed IP Address"
|
||||||
msgstr ""
|
msgstr "آی پی آدرس غیر مجاز"
|
||||||
|
|
|
@ -10,13 +10,14 @@
|
||||||
# Shun Yanaura <metroplexity@gmail.com>, 2016
|
# Shun Yanaura <metroplexity@gmail.com>, 2016
|
||||||
# Suguru Sato <usagi.vs.tanuki@gmail.com>, 2016
|
# Suguru Sato <usagi.vs.tanuki@gmail.com>, 2016
|
||||||
# Tomonori Tanabe <tanb+github@me.com>, 2015
|
# Tomonori Tanabe <tanb+github@me.com>, 2015
|
||||||
|
# Masaki Honda <mh35jp@gmail.com>, 2018
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
||||||
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
|
"PO-Revision-Date: 2018-12-28 01:50+0000\n"
|
||||||
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
|
"Last-Translator: Masaki Honda <mh35jp@gmail.com>\n"
|
||||||
"Language-Team: Japanese (http://www.transifex.com/taiga-agile-llc/taiga-back/"
|
"Language-Team: Japanese (http://www.transifex.com/taiga-agile-llc/taiga-back/"
|
||||||
"language/ja/)\n"
|
"language/ja/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -31,7 +32,7 @@ msgstr "パブリックなレジスタは無効です。"
|
||||||
|
|
||||||
#: taiga/auth/api.py:93
|
#: taiga/auth/api.py:93
|
||||||
msgid "You must accept our terms of service and privacy policy"
|
msgid "You must accept our terms of service and privacy policy"
|
||||||
msgstr ""
|
msgstr "利用規約とプライバシーポリシーに同意する必要があります"
|
||||||
|
|
||||||
#: taiga/auth/api.py:102
|
#: taiga/auth/api.py:102
|
||||||
msgid "invalid register type"
|
msgid "invalid register type"
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
# Translators:
|
# Translators:
|
||||||
# Dajo Hein, 2015
|
# Dajo Hein, 2015
|
||||||
# Haroun Pacquee, 2015
|
# Haroun Pacquee, 2015
|
||||||
|
# Joannes Anthonius Rommers <ja.rommers@lge.com>, 2018
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
||||||
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
|
"PO-Revision-Date: 2018-11-02 07:56+0000\n"
|
||||||
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
|
"Last-Translator: Joannes Anthonius Rommers <ja.rommers@lge.com>\n"
|
||||||
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
|
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
|
||||||
"language/nl/)\n"
|
"language/nl/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -27,7 +28,7 @@ msgstr "Publieke registratie is uitgeschakeld."
|
||||||
|
|
||||||
#: taiga/auth/api.py:93
|
#: taiga/auth/api.py:93
|
||||||
msgid "You must accept our terms of service and privacy policy"
|
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
|
#: taiga/auth/api.py:102
|
||||||
msgid "invalid register type"
|
msgid "invalid register type"
|
||||||
|
@ -55,7 +56,7 @@ msgstr "Gebruiker is al geregistreerd."
|
||||||
|
|
||||||
#: taiga/auth/services.py:141
|
#: taiga/auth/services.py:141
|
||||||
msgid "This user is already a member of the project."
|
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
|
#: taiga/auth/services.py:165
|
||||||
msgid "Error on creating new user."
|
msgid "Error on creating new user."
|
||||||
|
@ -105,7 +106,7 @@ msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/fields.py:638
|
#: taiga/base/api/fields.py:638
|
||||||
msgid "You email domain is not allowed"
|
msgid "You email domain is not allowed"
|
||||||
msgstr ""
|
msgstr "Jou e-mail domein is niet toegestaan"
|
||||||
|
|
||||||
#: taiga/base/api/fields.py:647
|
#: taiga/base/api/fields.py:647
|
||||||
msgid "Enter a valid email address."
|
msgid "Enter a valid email address."
|
||||||
|
@ -211,7 +212,7 @@ msgstr ""
|
||||||
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
|
#: taiga/projects/tasks/api.py:284 taiga/projects/userstories/api.py:346
|
||||||
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
|
#: taiga/projects/userstories/api.py:398 taiga/webhooks/api.py:71
|
||||||
msgid "Blocked element"
|
msgid "Blocked element"
|
||||||
msgstr ""
|
msgstr "Geblokkeerd element"
|
||||||
|
|
||||||
#: taiga/base/api/pagination.py:228
|
#: taiga/base/api/pagination.py:228
|
||||||
msgid "Page is not 'last', nor can it be converted to an int."
|
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
|
#: taiga/base/exceptions.py:219
|
||||||
msgid "No room left for more projects."
|
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
|
#: taiga/base/filters.py:105 taiga/base/filters.py:526
|
||||||
msgid "Error in filter params types."
|
msgid "Error in filter params types."
|
||||||
|
@ -486,6 +487,9 @@ msgid ""
|
||||||
"%(comment)s</p>\n"
|
"%(comment)s</p>\n"
|
||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"\n"
|
||||||
|
"<h3>commentaar:</h3>\n"
|
||||||
|
"<p>%(comment)s</p>"
|
||||||
|
|
||||||
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
#: taiga/base/templates/emails/updates-body-text.jinja:6
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -500,91 +504,91 @@ msgstr ""
|
||||||
|
|
||||||
#: taiga/base/utils/urls.py:68
|
#: taiga/base/utils/urls.py:68
|
||||||
msgid "Host access error"
|
msgid "Host access error"
|
||||||
msgstr ""
|
msgstr "Host toegang fout"
|
||||||
|
|
||||||
#: taiga/base/utils/urls.py:74
|
#: taiga/base/utils/urls.py:74
|
||||||
msgid "IP Address error"
|
msgid "IP Address error"
|
||||||
msgstr ""
|
msgstr "IP adres fout"
|
||||||
|
|
||||||
#: taiga/events/events.py:106
|
#: taiga/events/events.py:106
|
||||||
msgid "User story created"
|
msgid "User story created"
|
||||||
msgstr ""
|
msgstr "User story aangemaakt"
|
||||||
|
|
||||||
#: taiga/events/events.py:109
|
#: taiga/events/events.py:109
|
||||||
msgid "User story changed"
|
msgid "User story changed"
|
||||||
msgstr ""
|
msgstr "User story gewijzigd"
|
||||||
|
|
||||||
#: taiga/events/events.py:112
|
#: taiga/events/events.py:112
|
||||||
msgid "User story deleted"
|
msgid "User story deleted"
|
||||||
msgstr ""
|
msgstr "User story verwijderd"
|
||||||
|
|
||||||
#: taiga/events/events.py:114
|
#: taiga/events/events.py:114
|
||||||
msgid "US #{} - {}"
|
msgid "US #{} - {}"
|
||||||
msgstr ""
|
msgstr "US #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:117
|
#: taiga/events/events.py:117
|
||||||
msgid "Task created"
|
msgid "Task created"
|
||||||
msgstr ""
|
msgstr "Taak aangemaakt"
|
||||||
|
|
||||||
#: taiga/events/events.py:120
|
#: taiga/events/events.py:120
|
||||||
msgid "Task changed"
|
msgid "Task changed"
|
||||||
msgstr ""
|
msgstr "Taak gewijzigd"
|
||||||
|
|
||||||
#: taiga/events/events.py:123
|
#: taiga/events/events.py:123
|
||||||
msgid "Task deleted"
|
msgid "Task deleted"
|
||||||
msgstr ""
|
msgstr "Taak verwijderd"
|
||||||
|
|
||||||
#: taiga/events/events.py:125
|
#: taiga/events/events.py:125
|
||||||
msgid "Task #{} - {}"
|
msgid "Task #{} - {}"
|
||||||
msgstr ""
|
msgstr "Taak #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:128
|
#: taiga/events/events.py:128
|
||||||
msgid "Issue created"
|
msgid "Issue created"
|
||||||
msgstr ""
|
msgstr "Probleem aangemaakt"
|
||||||
|
|
||||||
#: taiga/events/events.py:131
|
#: taiga/events/events.py:131
|
||||||
msgid "Issue changed"
|
msgid "Issue changed"
|
||||||
msgstr ""
|
msgstr "Probleem gewijzigd"
|
||||||
|
|
||||||
#: taiga/events/events.py:134
|
#: taiga/events/events.py:134
|
||||||
msgid "Issue deleted"
|
msgid "Issue deleted"
|
||||||
msgstr ""
|
msgstr "Probleem verwijderd"
|
||||||
|
|
||||||
#: taiga/events/events.py:136
|
#: taiga/events/events.py:136
|
||||||
msgid "Issue: #{} - {}"
|
msgid "Issue: #{} - {}"
|
||||||
msgstr ""
|
msgstr "Probleem: #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:139
|
#: taiga/events/events.py:139
|
||||||
msgid "Wiki Page created"
|
msgid "Wiki Page created"
|
||||||
msgstr ""
|
msgstr "Wiki Pagina aangemaakt"
|
||||||
|
|
||||||
#: taiga/events/events.py:142
|
#: taiga/events/events.py:142
|
||||||
msgid "Wiki Page changed"
|
msgid "Wiki Page changed"
|
||||||
msgstr ""
|
msgstr "Wiki Pagina gewijzigd"
|
||||||
|
|
||||||
#: taiga/events/events.py:145
|
#: taiga/events/events.py:145
|
||||||
msgid "Wiki Page deleted"
|
msgid "Wiki Page deleted"
|
||||||
msgstr ""
|
msgstr "Wiki Pagina verwijderd"
|
||||||
|
|
||||||
#: taiga/events/events.py:147
|
#: taiga/events/events.py:147
|
||||||
msgid "Wiki Page: {}"
|
msgid "Wiki Page: {}"
|
||||||
msgstr ""
|
msgstr "Wiki pagina: {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:150
|
#: taiga/events/events.py:150
|
||||||
msgid "Sprint created"
|
msgid "Sprint created"
|
||||||
msgstr ""
|
msgstr "Sprint aangemaakt"
|
||||||
|
|
||||||
#: taiga/events/events.py:153
|
#: taiga/events/events.py:153
|
||||||
msgid "Sprint changed"
|
msgid "Sprint changed"
|
||||||
msgstr ""
|
msgstr "Sprint gewijzigd"
|
||||||
|
|
||||||
#: taiga/events/events.py:156
|
#: taiga/events/events.py:156
|
||||||
msgid "Sprint deleted"
|
msgid "Sprint deleted"
|
||||||
msgstr ""
|
msgstr "Sprint verwijderd"
|
||||||
|
|
||||||
#: taiga/events/events.py:158
|
#: taiga/events/events.py:158
|
||||||
msgid "Sprint: {}"
|
msgid "Sprint: {}"
|
||||||
msgstr ""
|
msgstr "Sprint: {}"
|
||||||
|
|
||||||
#: taiga/export_import/api.py:127
|
#: taiga/export_import/api.py:127
|
||||||
msgid "We needed at least one role"
|
msgid "We needed at least one role"
|
||||||
|
@ -621,23 +625,23 @@ msgstr "fout bij importeren van standaard projectattributen waarden"
|
||||||
|
|
||||||
#: taiga/export_import/services/store.py:779
|
#: taiga/export_import/services/store.py:779
|
||||||
msgid "error importing custom attributes"
|
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
|
#: taiga/export_import/services/store.py:783
|
||||||
msgid "error importing sprints"
|
msgid "error importing sprints"
|
||||||
msgstr "fout bij importeren sprints"
|
msgstr "fout bij het importeren van sprints"
|
||||||
|
|
||||||
#: taiga/export_import/services/store.py:787
|
#: taiga/export_import/services/store.py:787
|
||||||
msgid "error importing issues"
|
msgid "error importing issues"
|
||||||
msgstr "fout bij importeren issues"
|
msgstr "fout bij het importeren van problemen"
|
||||||
|
|
||||||
#: taiga/export_import/services/store.py:791
|
#: taiga/export_import/services/store.py:791
|
||||||
msgid "error importing user stories"
|
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
|
#: taiga/export_import/services/store.py:795
|
||||||
msgid "error importing epics"
|
msgid "error importing epics"
|
||||||
msgstr ""
|
msgstr "fout bij het importeren van epics"
|
||||||
|
|
||||||
#: taiga/export_import/services/store.py:799
|
#: taiga/export_import/services/store.py:799
|
||||||
msgid "error importing tasks"
|
msgid "error importing tasks"
|
||||||
|
@ -661,7 +665,7 @@ msgstr "fout bij importeren tijdlijnen"
|
||||||
|
|
||||||
#: taiga/export_import/services/store.py:837
|
#: taiga/export_import/services/store.py:837
|
||||||
msgid "unexpected error importing project"
|
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
|
#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63
|
||||||
msgid "Error generating project dump"
|
msgid "Error generating project dump"
|
||||||
|
@ -693,11 +697,11 @@ msgstr "Fout bij laden project dump"
|
||||||
|
|
||||||
#: taiga/export_import/tasks.py:121
|
#: taiga/export_import/tasks.py:121
|
||||||
msgid "Error loading your project dump file"
|
msgid "Error loading your project dump file"
|
||||||
msgstr ""
|
msgstr "Fout tijdens het laden van de project dump file"
|
||||||
|
|
||||||
#: taiga/export_import/tasks.py:135
|
#: taiga/export_import/tasks.py:135
|
||||||
msgid " -- no detail info --"
|
msgid " -- no detail info --"
|
||||||
msgstr ""
|
msgstr "-- geen gedetailleerde informatie --"
|
||||||
|
|
||||||
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
|
#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -893,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:43 taiga/external_apps/api.py:70
|
||||||
#: taiga/external_apps/api.py:77
|
#: taiga/external_apps/api.py:77
|
||||||
msgid "Authentication required"
|
msgid "Authentication required"
|
||||||
msgstr ""
|
msgstr "Authenticatie verreist"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:35
|
#: taiga/external_apps/models.py:35
|
||||||
#: taiga/projects/custom_attributes/models.py:36
|
#: taiga/projects/custom_attributes/models.py:36
|
||||||
|
@ -911,11 +915,11 @@ msgstr "naam"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:37
|
#: taiga/external_apps/models.py:37
|
||||||
msgid "Icon url"
|
msgid "Icon url"
|
||||||
msgstr ""
|
msgstr "Icoon url"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:38
|
#: taiga/external_apps/models.py:38
|
||||||
msgid "web"
|
msgid "web"
|
||||||
msgstr ""
|
msgstr "web"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:62
|
#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:62
|
||||||
#: taiga/projects/custom_attributes/models.py:37
|
#: taiga/projects/custom_attributes/models.py:37
|
||||||
|
@ -929,17 +933,17 @@ msgstr "omschrijving"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:41
|
#: taiga/external_apps/models.py:41
|
||||||
msgid "Next url"
|
msgid "Next url"
|
||||||
msgstr ""
|
msgstr "Volgende url"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:55 taiga/projects/contact/models.py:26
|
#: 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/likes/models.py:31 taiga/projects/notifications/models.py:88
|
||||||
#: taiga/projects/votes/models.py:52
|
#: taiga/projects/votes/models.py:52
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr "gebruiker"
|
||||||
|
|
||||||
#: taiga/external_apps/models.py:59
|
#: taiga/external_apps/models.py:59
|
||||||
msgid "application"
|
msgid "application"
|
||||||
msgstr ""
|
msgstr "applicatie"
|
||||||
|
|
||||||
#: taiga/feedback/models.py:25 taiga/users/models.py:147
|
#: taiga/feedback/models.py:25 taiga/users/models.py:147
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
# Lucas Boscaini <lucasboscaini@gmail.com>, 2017
|
# Lucas Boscaini <lucasboscaini@gmail.com>, 2017
|
||||||
# Mairieli Wessel <mairieliw@alunos.utfpr.edu.br>, 2016
|
# Mairieli Wessel <mairieliw@alunos.utfpr.edu.br>, 2016
|
||||||
# Marlon Carvalho <m.lopes@qiwi.com>, 2015
|
# 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
|
# Michel Wilhelm <michelwilhelm@gmail.com>, 2016
|
||||||
# pedromvm <pedromvm@gmail.com>, 2015
|
# pedromvm <pedromvm@gmail.com>, 2015
|
||||||
# Pedro Rangel Raft <me@pedroraft.com>, 2017
|
# Pedro Rangel Raft <me@pedroraft.com>, 2017
|
||||||
|
@ -30,8 +31,9 @@ msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
||||||
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
|
"PO-Revision-Date: 2018-11-17 17:51+0000\n"
|
||||||
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\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/"
|
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/"
|
||||||
"taiga-back/language/pt_BR/)\n"
|
"taiga-back/language/pt_BR/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -46,7 +48,7 @@ msgstr "Registro público está desabilitado. "
|
||||||
|
|
||||||
#: taiga/auth/api.py:93
|
#: taiga/auth/api.py:93
|
||||||
msgid "You must accept our terms of service and privacy policy"
|
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
|
#: taiga/auth/api.py:102
|
||||||
msgid "invalid register type"
|
msgid "invalid register type"
|
||||||
|
@ -525,83 +527,83 @@ msgstr ""
|
||||||
|
|
||||||
#: taiga/events/events.py:106
|
#: taiga/events/events.py:106
|
||||||
msgid "User story created"
|
msgid "User story created"
|
||||||
msgstr ""
|
msgstr "História de Usuário criada"
|
||||||
|
|
||||||
#: taiga/events/events.py:109
|
#: taiga/events/events.py:109
|
||||||
msgid "User story changed"
|
msgid "User story changed"
|
||||||
msgstr ""
|
msgstr "História de Usuário alterada"
|
||||||
|
|
||||||
#: taiga/events/events.py:112
|
#: taiga/events/events.py:112
|
||||||
msgid "User story deleted"
|
msgid "User story deleted"
|
||||||
msgstr ""
|
msgstr "História de Usuário excluída"
|
||||||
|
|
||||||
#: taiga/events/events.py:114
|
#: taiga/events/events.py:114
|
||||||
msgid "US #{} - {}"
|
msgid "US #{} - {}"
|
||||||
msgstr ""
|
msgstr "US #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:117
|
#: taiga/events/events.py:117
|
||||||
msgid "Task created"
|
msgid "Task created"
|
||||||
msgstr ""
|
msgstr "Tarefa criada"
|
||||||
|
|
||||||
#: taiga/events/events.py:120
|
#: taiga/events/events.py:120
|
||||||
msgid "Task changed"
|
msgid "Task changed"
|
||||||
msgstr ""
|
msgstr "Tarefa alterada"
|
||||||
|
|
||||||
#: taiga/events/events.py:123
|
#: taiga/events/events.py:123
|
||||||
msgid "Task deleted"
|
msgid "Task deleted"
|
||||||
msgstr ""
|
msgstr "Tarefa excluída"
|
||||||
|
|
||||||
#: taiga/events/events.py:125
|
#: taiga/events/events.py:125
|
||||||
msgid "Task #{} - {}"
|
msgid "Task #{} - {}"
|
||||||
msgstr ""
|
msgstr "Tarefa #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:128
|
#: taiga/events/events.py:128
|
||||||
msgid "Issue created"
|
msgid "Issue created"
|
||||||
msgstr ""
|
msgstr "Issue criada"
|
||||||
|
|
||||||
#: taiga/events/events.py:131
|
#: taiga/events/events.py:131
|
||||||
msgid "Issue changed"
|
msgid "Issue changed"
|
||||||
msgstr ""
|
msgstr "Issue alterada"
|
||||||
|
|
||||||
#: taiga/events/events.py:134
|
#: taiga/events/events.py:134
|
||||||
msgid "Issue deleted"
|
msgid "Issue deleted"
|
||||||
msgstr ""
|
msgstr "Issue excluída"
|
||||||
|
|
||||||
#: taiga/events/events.py:136
|
#: taiga/events/events.py:136
|
||||||
msgid "Issue: #{} - {}"
|
msgid "Issue: #{} - {}"
|
||||||
msgstr ""
|
msgstr "Issue: #{} - {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:139
|
#: taiga/events/events.py:139
|
||||||
msgid "Wiki Page created"
|
msgid "Wiki Page created"
|
||||||
msgstr ""
|
msgstr "Página Wiki criada"
|
||||||
|
|
||||||
#: taiga/events/events.py:142
|
#: taiga/events/events.py:142
|
||||||
msgid "Wiki Page changed"
|
msgid "Wiki Page changed"
|
||||||
msgstr ""
|
msgstr "Página Wiki alterada"
|
||||||
|
|
||||||
#: taiga/events/events.py:145
|
#: taiga/events/events.py:145
|
||||||
msgid "Wiki Page deleted"
|
msgid "Wiki Page deleted"
|
||||||
msgstr ""
|
msgstr "Página Wiki excluída"
|
||||||
|
|
||||||
#: taiga/events/events.py:147
|
#: taiga/events/events.py:147
|
||||||
msgid "Wiki Page: {}"
|
msgid "Wiki Page: {}"
|
||||||
msgstr ""
|
msgstr "Página Wiki: {}"
|
||||||
|
|
||||||
#: taiga/events/events.py:150
|
#: taiga/events/events.py:150
|
||||||
msgid "Sprint created"
|
msgid "Sprint created"
|
||||||
msgstr ""
|
msgstr "Sprint criada"
|
||||||
|
|
||||||
#: taiga/events/events.py:153
|
#: taiga/events/events.py:153
|
||||||
msgid "Sprint changed"
|
msgid "Sprint changed"
|
||||||
msgstr ""
|
msgstr "Sprint alterada"
|
||||||
|
|
||||||
#: taiga/events/events.py:156
|
#: taiga/events/events.py:156
|
||||||
msgid "Sprint deleted"
|
msgid "Sprint deleted"
|
||||||
msgstr ""
|
msgstr "Sprint excluída"
|
||||||
|
|
||||||
#: taiga/events/events.py:158
|
#: taiga/events/events.py:158
|
||||||
msgid "Sprint: {}"
|
msgid "Sprint: {}"
|
||||||
msgstr ""
|
msgstr "Sprint: {}"
|
||||||
|
|
||||||
#: taiga/export_import/api.py:127
|
#: taiga/export_import/api.py:127
|
||||||
msgid "We needed at least one role"
|
msgid "We needed at least one role"
|
||||||
|
@ -1279,7 +1281,7 @@ msgstr "O parâmetro url é necessário"
|
||||||
|
|
||||||
#: taiga/importers/jira/api.py:158
|
#: taiga/importers/jira/api.py:158
|
||||||
msgid "Invalid project_type {}"
|
msgid "Invalid project_type {}"
|
||||||
msgstr ""
|
msgstr "project_type inválido {}"
|
||||||
|
|
||||||
#: taiga/importers/jira/api.py:192
|
#: taiga/importers/jira/api.py:192
|
||||||
msgid "Invalid Jira server configuration."
|
msgid "Invalid Jira server configuration."
|
||||||
|
@ -1518,7 +1520,7 @@ msgstr "Pedido invalido: %s há %s"
|
||||||
#: taiga/importers/trello/importer.py:80 taiga/importers/trello/importer.py:82
|
#: taiga/importers/trello/importer.py:80 taiga/importers/trello/importer.py:82
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Unauthorized: %s at %s"
|
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
|
#: taiga/importers/trello/importer.py:84 taiga/importers/trello/importer.py:86
|
||||||
#, python-format
|
#, python-format
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,6 +4,7 @@
|
||||||
#
|
#
|
||||||
# Translators:
|
# Translators:
|
||||||
# Translators:
|
# Translators:
|
||||||
|
# Andy zhan <daliangzao189@126.com>, 2018
|
||||||
# Ares <yangzibin.cn@hotmail.com>, 2017
|
# Ares <yangzibin.cn@hotmail.com>, 2017
|
||||||
# gm l <linguangmo@gmail.com>, 2016
|
# gm l <linguangmo@gmail.com>, 2016
|
||||||
# Hanbing Yin <yin_suk@hotmail.com>, 2016
|
# Hanbing Yin <yin_suk@hotmail.com>, 2016
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
# Yang Yu <yuyangmkdir@yahoo.com>, 2016
|
# Yang Yu <yuyangmkdir@yahoo.com>, 2016
|
||||||
# yonee <yangqo@hotmail.com>, 2015
|
# yonee <yangqo@hotmail.com>, 2015
|
||||||
# 5791113 <yonglong.ma@outlook.com>, 2016
|
# 5791113 <yonglong.ma@outlook.com>, 2016
|
||||||
# yungang <zangyungang@gmail.com>, 2018
|
# zangyg <zangyungang@gmail.com>, 2018
|
||||||
# 5791113 <yonglong.ma@outlook.com>, 2016
|
# 5791113 <yonglong.ma@outlook.com>, 2016
|
||||||
# 朱坚 <garyzhu2009@gmail.com>, 2017
|
# 朱坚 <garyzhu2009@gmail.com>, 2017
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -25,8 +26,8 @@ msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
"POT-Creation-Date: 2018-10-11 14:11+0200\n"
|
||||||
"PO-Revision-Date: 2018-10-14 17:29+0000\n"
|
"PO-Revision-Date: 2018-12-16 07:10+0000\n"
|
||||||
"Last-Translator: Alejandro Hermida <alexhermida@gmail.com>\n"
|
"Last-Translator: Andy zhan <daliangzao189@126.com>\n"
|
||||||
"Language-Team: Chinese Simplified (http://www.transifex.com/taiga-agile-llc/"
|
"Language-Team: Chinese Simplified (http://www.transifex.com/taiga-agile-llc/"
|
||||||
"taiga-back/language/zh-Hans/)\n"
|
"taiga-back/language/zh-Hans/)\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -4649,11 +4650,11 @@ msgstr "加入日期"
|
||||||
|
|
||||||
#: taiga/users/models.py:155
|
#: taiga/users/models.py:155
|
||||||
msgid "accepted terms"
|
msgid "accepted terms"
|
||||||
msgstr ""
|
msgstr "已接受的条款"
|
||||||
|
|
||||||
#: taiga/users/models.py:156
|
#: taiga/users/models.py:156
|
||||||
msgid "new terms read"
|
msgid "new terms read"
|
||||||
msgstr ""
|
msgstr "阅读新条款"
|
||||||
|
|
||||||
#: taiga/users/models.py:158
|
#: taiga/users/models.py:158
|
||||||
msgid "default language"
|
msgid "default language"
|
||||||
|
@ -4894,7 +4895,7 @@ msgstr "无效用户名,请尝试其他的。"
|
||||||
|
|
||||||
#: taiga/users/validators.py:73
|
#: taiga/users/validators.py:73
|
||||||
msgid "Read new terms has to be true'"
|
msgid "Read new terms has to be true'"
|
||||||
msgstr ""
|
msgstr "还没有阅读新条款"
|
||||||
|
|
||||||
#: taiga/userstorage/api.py:53
|
#: taiga/userstorage/api.py:53
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -4939,4 +4940,4 @@ msgstr "持续时间"
|
||||||
|
|
||||||
#: taiga/webhooks/validators.py:42
|
#: taiga/webhooks/validators.py:42
|
||||||
msgid "Not allowed IP Address"
|
msgid "Not allowed IP Address"
|
||||||
msgstr ""
|
msgstr "IP地址被禁用"
|
||||||
|
|
|
@ -37,7 +37,7 @@ class AutolinkExtension(markdown.Extension):
|
||||||
* GitHub only accepts URLs with protocols or "www.", whereas Gruber's regex
|
* GitHub only accepts URLs with protocols or "www.", whereas Gruber's regex
|
||||||
accepts things like "foo.com/bar".
|
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<>]+))'
|
url_re = r'(?i)\b((?:(?:ftp|https?)://|www\d{0,3}[.])([^\s<>]+))'
|
||||||
autolink = AutolinkPattern(url_re, md)
|
autolink = AutolinkPattern(url_re, md)
|
||||||
md.inlinePatterns.add('gfm-autolink', autolink, '_end')
|
md.inlinePatterns.add('gfm-autolink', autolink, '_end')
|
||||||
|
|
|
@ -19,7 +19,7 @@ class AutomailPattern(markdown.inlinepatterns.Pattern):
|
||||||
class AutomailExtension(markdown.Extension):
|
class AutomailExtension(markdown.Extension):
|
||||||
"""An extension that turns all email addresses into links."""
|
"""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'
|
mail_re = r'\b(?i)([a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]+)\b'
|
||||||
automail = AutomailPattern(mail_re, md)
|
automail = AutomailPattern(mail_re, md)
|
||||||
md.inlinePatterns.add('gfm-automail', automail, '_end')
|
md.inlinePatterns.add('gfm-automail', automail, '_end')
|
||||||
|
|
|
@ -152,7 +152,7 @@ EMOJIS_SET = {
|
||||||
|
|
||||||
class EmojifyExtension(Extension):
|
class EmojifyExtension(Extension):
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
md.registerExtension(self)
|
md.registerExtension(self)
|
||||||
md.preprocessors.add('emojify',
|
md.preprocessors.add('emojify',
|
||||||
EmojifyPreprocessor(md),
|
EmojifyPreprocessor(md),
|
||||||
|
|
|
@ -30,7 +30,7 @@ from markdown.util import etree, AtomicString
|
||||||
|
|
||||||
|
|
||||||
class MentionsExtension(Extension):
|
class MentionsExtension(Extension):
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
MENTION_RE = r"(@)([\w.-]+)"
|
MENTION_RE = r"(@)([\w.-]+)"
|
||||||
mentionsPattern = MentionsPattern(MENTION_RE)
|
mentionsPattern = MentionsPattern(MENTION_RE)
|
||||||
mentionsPattern.md = md
|
mentionsPattern.md = md
|
||||||
|
|
|
@ -36,7 +36,7 @@ class TaigaReferencesExtension(Extension):
|
||||||
self.project = project
|
self.project = project
|
||||||
return super().__init__(*args, **kwargs)
|
return super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
TAIGA_REFERENCE_RE = r'(?<=^|(?<=[^a-zA-Z0-9-\[]))#(\d+)'
|
TAIGA_REFERENCE_RE = r'(?<=^|(?<=[^a-zA-Z0-9-\[]))#(\d+)'
|
||||||
referencesPattern = TaigaReferencesPattern(TAIGA_REFERENCE_RE, self.project)
|
referencesPattern = TaigaReferencesPattern(TAIGA_REFERENCE_RE, self.project)
|
||||||
referencesPattern.md = md
|
referencesPattern.md = md
|
||||||
|
|
|
@ -28,6 +28,6 @@ class SemiSaneListExtension(markdown.Extension):
|
||||||
newlines.
|
newlines.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
md.parser.blockprocessors['olist'] = SemiSaneOListProcessor(md.parser)
|
md.parser.blockprocessors['olist'] = SemiSaneOListProcessor(md.parser)
|
||||||
md.parser.blockprocessors['ulist'] = SemiSaneUListProcessor(md.parser)
|
md.parser.blockprocessors['ulist'] = SemiSaneUListProcessor(md.parser)
|
||||||
|
|
|
@ -31,7 +31,7 @@ class SpacedLinkExtension(markdown.Extension):
|
||||||
extension adds such support.
|
extension adds such support.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
md.inlinePatterns["link"] = \
|
md.inlinePatterns["link"] = \
|
||||||
markdown.inlinepatterns.LinkPattern(SPACED_LINK_RE, md)
|
markdown.inlinepatterns.LinkPattern(SPACED_LINK_RE, md)
|
||||||
md.inlinePatterns["reference"] = \
|
md.inlinePatterns["reference"] = \
|
||||||
|
|
|
@ -14,6 +14,6 @@ class StrikethroughExtension(markdown.Extension):
|
||||||
For example: ``~~strike~~``.
|
For example: ``~~strike~~``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
pattern = markdown.inlinepatterns.SimpleTagPattern(STRIKE_RE, 'del')
|
pattern = markdown.inlinepatterns.SimpleTagPattern(STRIKE_RE, 'del')
|
||||||
md.inlinePatterns.add('gfm-strikethrough', pattern, '_end')
|
md.inlinePatterns.add('gfm-strikethrough', pattern, '_end')
|
||||||
|
|
|
@ -28,7 +28,7 @@ from taiga.front.templatetags.functions import resolve
|
||||||
|
|
||||||
class TargetBlankLinkExtension(markdown.Extension):
|
class TargetBlankLinkExtension(markdown.Extension):
|
||||||
"""An extension that add target="_blank" to all external links."""
|
"""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",
|
md.treeprocessors.add("target_blank_links",
|
||||||
TargetBlankLinksTreeprocessor(md),
|
TargetBlankLinksTreeprocessor(md),
|
||||||
"<prettify")
|
"<prettify")
|
||||||
|
|
|
@ -34,7 +34,7 @@ class WikiLinkExtension(Extension):
|
||||||
self.project = project
|
self.project = project
|
||||||
return super().__init__(*args, **kwargs)
|
return super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def extendMarkdown(self, md, md_globals):
|
def extendMarkdown(self, md):
|
||||||
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
|
WIKILINK_RE = r"\[\[([\w0-9_ -]+)(\|[^\]]+)?\]\]"
|
||||||
md.inlinePatterns.add("wikilinks",
|
md.inlinePatterns.add("wikilinks",
|
||||||
WikiLinksPattern(md, WIKILINK_RE, self.project),
|
WikiLinksPattern(md, WIKILINK_RE, self.project),
|
||||||
|
|
|
@ -45,6 +45,7 @@ from taiga.projects.epics.models import Epic
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
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.mixins import WatchersViewSetMixin
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
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_notify_policies(qs)
|
||||||
qs = project_utils.attach_is_fan(qs, user=self.request.user)
|
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_my_role_permissions(qs, user=self.request.user)
|
|
||||||
qs = project_utils.attach_closed_milestones(qs)
|
qs = project_utils.attach_closed_milestones(qs)
|
||||||
|
qs = project_utils.attach_my_homepage(qs, user=self.request.user)
|
||||||
else:
|
else:
|
||||||
qs = project_utils.attach_extra_info(qs, user=self.request.user)
|
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,
|
invitation_extra_text=invitation_extra_text,
|
||||||
callback=self.post_save,
|
callback=self.post_save,
|
||||||
precall=self.pre_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:
|
except exc.ValidationError as err:
|
||||||
return response.BadRequest(err.message_dict)
|
return response.BadRequest(err.message_dict)
|
||||||
|
|
||||||
|
|
|
@ -53,17 +53,17 @@ def connect_memberships_signals():
|
||||||
sender=apps.get_model("projects", "Membership"),
|
sender=apps.get_model("projects", "Membership"),
|
||||||
dispatch_uid='membership_pre_delete')
|
dispatch_uid='membership_pre_delete')
|
||||||
|
|
||||||
# On membership object is deleted, update notify policies of all objects relation.
|
# On membership object is created, reorder and create notify policies
|
||||||
signals.post_save.connect(handlers.create_notify_policy,
|
signals.post_save.connect(handlers.membership_post_save,
|
||||||
sender=apps.get_model("projects", "Membership"),
|
sender=apps.get_model("projects", "Membership"),
|
||||||
dispatch_uid='create-notify-policy')
|
dispatch_uid='membership_post_save')
|
||||||
|
|
||||||
|
|
||||||
def disconnect_memberships_signals():
|
def disconnect_memberships_signals():
|
||||||
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"),
|
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"),
|
||||||
dispatch_uid='membership_pre_delete')
|
dispatch_uid='membership_pre_delete')
|
||||||
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"),
|
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"),
|
||||||
dispatch_uid='create-notify-policy')
|
dispatch_uid='membership_post_save')
|
||||||
|
|
||||||
|
|
||||||
## US Statuses Signals
|
## US Statuses Signals
|
||||||
|
|
|
@ -24,11 +24,17 @@ MULTILINE_TYPE = "multiline"
|
||||||
RICHTEXT_TYPE = "richtext"
|
RICHTEXT_TYPE = "richtext"
|
||||||
DATE_TYPE = "date"
|
DATE_TYPE = "date"
|
||||||
URL_TYPE = "url"
|
URL_TYPE = "url"
|
||||||
|
DROPDOWN_TYPE = "dropdown"
|
||||||
|
CHECKBOX_TYPE = "checkbox"
|
||||||
|
NUMBER_TYPE = "number"
|
||||||
|
|
||||||
TYPES_CHOICES = (
|
TYPES_CHOICES = (
|
||||||
(TEXT_TYPE, _("Text")),
|
(TEXT_TYPE, _("Text")),
|
||||||
(MULTILINE_TYPE, _("Multi-Line Text")),
|
(MULTILINE_TYPE, _("Multi-Line Text")),
|
||||||
(RICHTEXT_TYPE, _("Rich text")),
|
(RICHTEXT_TYPE, _("Rich text")),
|
||||||
(DATE_TYPE, _("Date")),
|
(DATE_TYPE, _("Date")),
|
||||||
(URL_TYPE, _("Url"))
|
(URL_TYPE, _("Url")),
|
||||||
|
(DROPDOWN_TYPE, _("Dropdown")),
|
||||||
|
(CHECKBOX_TYPE, _("Checkbox")),
|
||||||
|
(NUMBER_TYPE, _("Number")),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -41,7 +41,7 @@ class AbstractCustomAttribute(models.Model):
|
||||||
order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order"))
|
order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order"))
|
||||||
project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss",
|
project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss",
|
||||||
verbose_name=_("project"))
|
verbose_name=_("project"))
|
||||||
|
extra = JSONField(blank=True, default=None, null=True)
|
||||||
created_date = models.DateTimeField(null=False, blank=False, default=timezone.now,
|
created_date = models.DateTimeField(null=False, blank=False, default=timezone.now,
|
||||||
verbose_name=_("created date"))
|
verbose_name=_("created date"))
|
||||||
modified_date = models.DateTimeField(null=False, blank=False,
|
modified_date = models.DateTimeField(null=False, blank=False,
|
||||||
|
|
|
@ -32,6 +32,7 @@ class BaseCustomAttributeSerializer(serializers.LightSerializer):
|
||||||
type = Field()
|
type = Field()
|
||||||
order = Field()
|
order = Field()
|
||||||
project = Field(attr="project_id")
|
project = Field(attr="project_id")
|
||||||
|
extra = Field()
|
||||||
created_date = Field()
|
created_date = Field()
|
||||||
modified_date = Field()
|
modified_date = Field()
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ from taiga.base import response
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.api import ReadOnlyListViewSet
|
from taiga.base.api import ReadOnlyListViewSet
|
||||||
from taiga.mdrender.service import render as mdrender
|
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 permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -57,6 +59,11 @@ class HistoryViewSet(ReadOnlyListViewSet):
|
||||||
|
|
||||||
return response.Ok(serializer.data)
|
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'])
|
@detail_route(methods=['get'])
|
||||||
def comment_versions(self, request, pk):
|
def comment_versions(self, request, pk):
|
||||||
obj = self.get_object()
|
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.edit_comment_date = timezone.now()
|
||||||
history_entry.comment = comment
|
history_entry.comment = comment
|
||||||
history_entry.comment_html = mdrender(obj.project, comment)
|
history_entry.comment_html = mdrender(obj.project, comment)
|
||||||
history_entry.comment_versions = comment_versions
|
history_entry.comment_versions = comment_versions
|
||||||
history_entry.save()
|
history_entry.save()
|
||||||
|
|
||||||
|
if new_mentions:
|
||||||
|
signal_mentions.send(sender=self.__class__,
|
||||||
|
user=self.request.user,
|
||||||
|
obj=obj,
|
||||||
|
mentions=new_mentions)
|
||||||
|
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=['post'])
|
||||||
|
@ -165,6 +181,17 @@ class HistoryViewSet(ReadOnlyListViewSet):
|
||||||
self.check_permissions(request, "retrieve", obj)
|
self.check_permissions(request, "retrieve", obj)
|
||||||
qs = services.get_history_queryset_by_model_instance(obj)
|
qs = services.get_history_queryset_by_model_instance(obj)
|
||||||
qs = services.prefetch_owners_in_history_queryset(qs)
|
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)
|
return self.response_for_queryset(qs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ from .choices import HistoryType
|
||||||
from .choices import HISTORY_TYPE_CHOICES
|
from .choices import HISTORY_TYPE_CHOICES
|
||||||
|
|
||||||
from taiga.base.utils.diff import make_diff as make_diff_from_dicts
|
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
|
# 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
|
# 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:
|
if aid in oldcustattrs and aid in newcustattrs:
|
||||||
changes = make_diff_from_dicts(oldcustattrs[aid], newcustattrs[aid],
|
changes = make_diff_from_dicts(oldcustattrs[aid], newcustattrs[aid],
|
||||||
excluded_keys=("name"))
|
excluded_keys=("name"))
|
||||||
|
|
||||||
newcustattr = newcustattrs.get(aid, {})
|
newcustattr = newcustattrs.get(aid, {})
|
||||||
if changes:
|
if changes:
|
||||||
change_type = newcustattr.get("type", TEXT_TYPE)
|
change_type = newcustattr.get("type", TEXT_TYPE)
|
||||||
old_value = oldcustattrs[aid].get("value", "")
|
|
||||||
new_value = newcustattrs[aid].get("value", "")
|
if change_type in [NUMBER_TYPE, CHECKBOX_TYPE]:
|
||||||
value_diff = get_diff_of_htmls(old_value, new_value)
|
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 = {
|
change = {
|
||||||
"name": newcustattr.get("name", ""),
|
"name": newcustattr.get("name", ""),
|
||||||
"changes": changes,
|
"changes": changes,
|
||||||
|
@ -279,8 +285,15 @@ class HistoryEntry(models.Model):
|
||||||
elif aid in oldcustattrs and aid not in newcustattrs:
|
elif aid in oldcustattrs and aid not in newcustattrs:
|
||||||
custom_attributes["deleted"].append(oldcustattrs[aid])
|
custom_attributes["deleted"].append(oldcustattrs[aid])
|
||||||
elif aid not in oldcustattrs and aid in newcustattrs:
|
elif aid not in oldcustattrs and aid in newcustattrs:
|
||||||
new_value = newcustattrs[aid].get("value", "")
|
newcustattr = newcustattrs.get(aid, {})
|
||||||
value_diff = get_diff_of_htmls("", new_value)
|
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
|
newcustattrs[aid]["value_diff"] = value_diff
|
||||||
custom_attributes["new"].append(newcustattrs[aid])
|
custom_attributes["new"].append(newcustattrs[aid])
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,12 @@ from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
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.mixins.by_ref import ByRefMixin
|
||||||
from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType
|
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.occ import OCCResourceMixin
|
||||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
@ -44,8 +47,10 @@ from . import serializers
|
||||||
from . import validators
|
from . import validators
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class IssueViewSet(AssignedToSignalMixin, OCCResourceMixin, VotedResourceMixin,
|
||||||
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
HistoryResourceMixin, WatchedResourceMixin, ByRefMixin,
|
||||||
|
TaggedResourceMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet):
|
||||||
validator_class = validators.IssueValidator
|
validator_class = validators.IssueValidator
|
||||||
queryset = models.Issue.objects.all()
|
queryset = models.Issue.objects.all()
|
||||||
permission_classes = (permissions.IssuePermission, )
|
permission_classes = (permissions.IssuePermission, )
|
||||||
|
@ -248,6 +253,22 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
|
|
||||||
return response.BadRequest(validator.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
|
@list_route(methods=["POST"])
|
||||||
|
def bulk_update_milestone(self, request, **kwargs):
|
||||||
|
validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
|
||||||
|
if not validator.is_valid():
|
||||||
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
|
data = validator.data
|
||||||
|
project = get_object_or_404(Project, pk=data["project_id"])
|
||||||
|
milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
|
||||||
|
|
||||||
|
self.check_permissions(request, "bulk_update_milestone", project)
|
||||||
|
|
||||||
|
ret = services.update_issues_milestone_in_bulk(data["bulk_issues"], milestone)
|
||||||
|
|
||||||
|
return response.Ok(ret)
|
||||||
|
|
||||||
|
|
||||||
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
|
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
|
||||||
permission_classes = (permissions.IssueVotersPermission,)
|
permission_classes = (permissions.IssueVotersPermission,)
|
||||||
|
|
|
@ -25,6 +25,11 @@ def connect_issues_signals():
|
||||||
from taiga.projects.tagging import signals as tagging_handlers
|
from taiga.projects.tagging import signals as tagging_handlers
|
||||||
from . import signals as 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
|
# Finished date
|
||||||
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
|
signals.pre_save.connect(handlers.set_finished_date_when_edit_issue,
|
||||||
sender=apps.get_model("issues", "Issue"),
|
sender=apps.get_model("issues", "Issue"),
|
||||||
|
@ -35,6 +40,14 @@ def connect_issues_signals():
|
||||||
sender=apps.get_model("issues", "Issue"),
|
sender=apps.get_model("issues", "Issue"),
|
||||||
dispatch_uid="tags_normalization_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():
|
def connect_issues_custom_attributes_signals():
|
||||||
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
|
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
|
||||||
|
@ -50,11 +63,19 @@ def connect_all_issues_signals():
|
||||||
|
|
||||||
|
|
||||||
def disconnect_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"),
|
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
||||||
dispatch_uid="set_finished_date_when_edit_issue")
|
dispatch_uid="set_finished_date_when_edit_issue")
|
||||||
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
||||||
dispatch_uid="tags_normalization_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():
|
def disconnect_issues_custom_attributes_signals():
|
||||||
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"),
|
||||||
|
|
|
@ -35,6 +35,7 @@ class IssuePermission(TaigaResourcePermission):
|
||||||
filters_data_perms = AllowAny()
|
filters_data_perms = AllowAny()
|
||||||
csv_perms = AllowAny()
|
csv_perms = AllowAny()
|
||||||
bulk_create_perms = HasProjectPerm('add_issue')
|
bulk_create_perms = HasProjectPerm('add_issue')
|
||||||
|
bulk_update_milestone_perms = HasProjectPerm('modify_issue')
|
||||||
delete_comment_perms= HasProjectPerm('modify_issue')
|
delete_comment_perms= HasProjectPerm('modify_issue')
|
||||||
upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
upvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
||||||
downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
downvote_perms = IsAuthenticated() & HasProjectPerm('view_issues')
|
||||||
|
|
|
@ -26,6 +26,9 @@ from django.db import connection
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.utils import db, text
|
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 (
|
from taiga.projects.issues.apps import (
|
||||||
connect_issues_signals,
|
connect_issues_signals,
|
||||||
disconnect_issues_signals)
|
disconnect_issues_signals)
|
||||||
|
@ -72,10 +75,38 @@ def create_issues_in_bulk(bulk_data, callback=None, precall=None, **additional_f
|
||||||
return issues
|
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
|
# CSV
|
||||||
#####################################################
|
#####################################################
|
||||||
|
|
||||||
|
|
||||||
def issues_to_csv(project, queryset):
|
def issues_to_csv(project, queryset):
|
||||||
csv_data = io.StringIO()
|
csv_data = io.StringIO()
|
||||||
fieldnames = ["id", "ref", "subject", "description", "sprint_id", "sprint",
|
fieldnames = ["id", "ref", "subject", "description", "sprint_id", "sprint",
|
||||||
|
|
|
@ -16,9 +16,22 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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
|
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
|
# Signals for set finished date
|
||||||
####################################
|
####################################
|
||||||
|
@ -30,3 +43,48 @@ def set_finished_date_when_edit_issue(sender, instance, **kwargs):
|
||||||
instance.finished_date = timezone.now()
|
instance.finished_date = timezone.now()
|
||||||
elif not instance.status.is_closed and instance.finished_date:
|
elif not instance.status.is_closed and instance.finished_date:
|
||||||
instance.finished_date = None
|
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)
|
||||||
|
|
|
@ -15,10 +15,13 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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 serializers
|
||||||
from taiga.base.api import validators
|
from taiga.base.api import validators
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
|
from taiga.projects.milestones.models import Milestone
|
||||||
from taiga.projects.mixins.validators import AssignedToValidator
|
from taiga.projects.mixins.validators import AssignedToValidator
|
||||||
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
|
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
|
@ -43,3 +46,35 @@ class IssuesBulkValidator(ProjectExistsValidator, validators.Validator):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
milestone_id = serializers.IntegerField(required=False)
|
milestone_id = serializers.IntegerField(required=False)
|
||||||
bulk_issues = serializers.CharField()
|
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
|
||||||
|
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -27,11 +27,17 @@ from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.utils.db import get_object_or_none
|
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 WatchedResourceMixin
|
||||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
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 serializers
|
||||||
|
from . import services
|
||||||
from . import validators
|
from . import validators
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
@ -142,6 +148,69 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
|
||||||
return response.Ok(milestone_stats)
|
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):
|
class MilestoneWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
|
||||||
permission_classes = (permissions.MilestoneWatchersPermission,)
|
permission_classes = (permissions.MilestoneWatchersPermission,)
|
||||||
|
|
|
@ -33,6 +33,11 @@ class MilestonePermission(TaigaResourcePermission):
|
||||||
stats_perms = HasProjectPerm('view_milestones')
|
stats_perms = HasProjectPerm('view_milestones')
|
||||||
watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
|
watch_perms = IsAuthenticated() & HasProjectPerm('view_milestones')
|
||||||
unwatch_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):
|
class MilestoneWatchersPermission(TaigaResourcePermission):
|
||||||
enought_perms = IsProjectAdmin() | IsSuperUser()
|
enought_perms = IsProjectAdmin() | IsSuperUser()
|
||||||
|
|
|
@ -16,16 +16,29 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.utils import timezone
|
from taiga.base.utils import db
|
||||||
|
from taiga.events import events
|
||||||
from . import models
|
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):
|
def calculate_milestone_is_closed(milestone):
|
||||||
return (milestone.user_stories.all().count() > 0 and
|
all_us_closed = all([user_story.is_closed for user_story in milestone.user_stories.all()])
|
||||||
all([task.status is not None and task.status.is_closed for task in milestone.tasks.all()]) and
|
all_tasks_closed = all([task.status is not None and task.status.is_closed for task in
|
||||||
all([user_story.is_closed for user_story in milestone.user_stories.all()]))
|
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):
|
def close_milestone(milestone):
|
||||||
|
@ -38,3 +51,137 @@ def open_milestone(milestone):
|
||||||
if milestone.closed:
|
if milestone.closed:
|
||||||
milestone.closed = False
|
milestone.closed = False
|
||||||
milestone.save(update_fields=["closed",])
|
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
|
||||||
|
|
|
@ -19,15 +19,17 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.exceptions import ValidationError
|
from taiga.base.exceptions import ValidationError
|
||||||
|
from taiga.base.api import serializers
|
||||||
from taiga.base.api import validators
|
from taiga.base.api import validators
|
||||||
from taiga.projects.validators import DuplicatedNameInProjectValidator
|
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
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
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class MilestoneExistsValidator:
|
class MilestoneExistsValidator:
|
||||||
def validate_sprint_id(self, attrs, source):
|
def validate_milestone_id(self, attrs, source):
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.Milestone.objects.filter(pk=value).exists():
|
if not models.Milestone.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no milestone with that id")
|
msg = _("There's no milestone with that id")
|
||||||
|
@ -39,3 +41,28 @@ class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, va
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Milestone
|
model = models.Milestone
|
||||||
read_only_fields = ("id", "created_date", "modified_date")
|
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
|
||||||
|
|
|
@ -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"
|
|
@ -17,8 +17,12 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.db.models import Q
|
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 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.notifications.choices import NotifyLevel
|
||||||
from taiga.projects.models import Project
|
from taiga.projects.models import Project
|
||||||
|
@ -50,3 +54,53 @@ class NotifyPolicyViewSet(ModelCrudViewSet):
|
||||||
return models.NotifyPolicy.objects.filter(user=self.request.user).filter(
|
return models.NotifyPolicy.objects.filter(user=self.request.user).filter(
|
||||||
Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user)
|
Q(project__owner=self.request.user) | Q(project__memberships__user=self.request.user)
|
||||||
).distinct()
|
).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()
|
||||||
|
|
|
@ -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)
|
|
@ -31,3 +31,22 @@ NOTIFY_LEVEL_CHOICES = (
|
||||||
(NotifyLevel.all, _("All")),
|
(NotifyLevel.all, _("All")),
|
||||||
(NotifyLevel.none, _("None")),
|
(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")),
|
||||||
|
)
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -29,6 +29,12 @@ from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.fields import WatchersField, MethodField
|
from taiga.base.fields import WatchersField, MethodField
|
||||||
from taiga.projects.notifications import services
|
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
|
from . serializers import WatcherSerializer
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,6 +53,8 @@ class WatchedResourceMixin:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_not_notify = False
|
_not_notify = False
|
||||||
|
_old_watchers = None
|
||||||
|
_old_mentions = []
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def watch(self, request, pk=None):
|
def watch(self, request, pk=None):
|
||||||
|
@ -86,13 +94,38 @@ class WatchedResourceMixin:
|
||||||
# some text fields for extract mentions and add them
|
# some text fields for extract mentions and add them
|
||||||
# to watchers before obtain a complete list of
|
# to watchers before obtain a complete list of
|
||||||
# notifiable users.
|
# 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
|
# Get a complete list of notifiable users for current
|
||||||
# object and send the change notification to them.
|
# object and send the change notification to them.
|
||||||
services.send_notifications(obj, history=history)
|
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):
|
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)
|
self.send_notifications(obj)
|
||||||
super().post_save(obj, created)
|
super().post_save(obj, created)
|
||||||
|
|
||||||
|
@ -100,6 +133,84 @@ class WatchedResourceMixin:
|
||||||
self.send_notifications(obj)
|
self.send_notifications(obj)
|
||||||
super().pre_delete(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):
|
class WatchedModelMixin(object):
|
||||||
"""
|
"""
|
||||||
|
@ -274,3 +385,47 @@ class WatchersViewSetMixin:
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
|
resource = self.resource_model.objects.get(pk=self.kwargs.get("resource_id"))
|
||||||
return resource.get_watchers()
|
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
|
||||||
|
|
|
@ -23,6 +23,7 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from taiga.base.db.models.fields import JSONField
|
||||||
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
|
from taiga.projects.history.choices import HISTORY_TYPE_CHOICES
|
||||||
|
|
||||||
from .choices import NOTIFY_LEVEL_CHOICES, NotifyLevel
|
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")
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="notify_policies")
|
||||||
notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES)
|
notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES)
|
||||||
live_notify_level = models.SmallIntegerField(choices=NOTIFY_LEVEL_CHOICES, default=NotifyLevel.involved)
|
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)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
modified_at = models.DateTimeField()
|
modified_at = models.DateTimeField()
|
||||||
|
@ -94,3 +96,11 @@ class Watched(models.Model):
|
||||||
verbose_name = _("Watched")
|
verbose_name = _("Watched")
|
||||||
verbose_name_plural = _("Watched")
|
verbose_name_plural = _("Watched")
|
||||||
unique_together = ("content_type", "object_id", "user", "project")
|
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()
|
||||||
|
|
|
@ -16,8 +16,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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.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.models import get_user_model_safe
|
||||||
|
from taiga.users.services import get_user_photo_url, get_user_big_photo_url
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
@ -27,7 +32,8 @@ class NotifyPolicySerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.NotifyPolicy
|
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):
|
def get_project_name(self, obj):
|
||||||
return obj.project.name
|
return obj.project.name
|
||||||
|
@ -39,3 +45,67 @@ class WatcherSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = get_user_model_safe()
|
model = get_user_model_safe()
|
||||||
fields = ('id', 'username', 'full_name')
|
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()
|
||||||
|
|
|
@ -44,6 +44,10 @@ from .models import HistoryChangeNotification, Watched
|
||||||
from .squashing import squash_history_entries
|
from .squashing import squash_history_entries
|
||||||
|
|
||||||
|
|
||||||
|
def remove_lr_cr(s):
|
||||||
|
return s.replace("\n", "").replace("\r", "")
|
||||||
|
|
||||||
|
|
||||||
def notify_policy_exists(project, user) -> bool:
|
def notify_policy_exists(project, user) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if policy exists for specified project
|
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,
|
def create_notify_policy_if_not_exists(project, user,
|
||||||
level=NotifyLevel.involved,
|
level=NotifyLevel.involved,
|
||||||
live_level=NotifyLevel.involved):
|
live_level=NotifyLevel.involved,
|
||||||
|
web_level=True):
|
||||||
"""
|
"""
|
||||||
Given a project and user, create notification policy for it.
|
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(
|
result = model_cls.objects.get_or_create(
|
||||||
project=project,
|
project=project,
|
||||||
user=user,
|
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]
|
return result[0]
|
||||||
except IntegrityError as e:
|
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
|
Generic implementation for analize model objects and
|
||||||
extract mentions from it and add it to watchers.
|
extract mentions from it and add it to watchers.
|
||||||
"""
|
"""
|
||||||
|
if not hasattr(obj, "add_watcher"):
|
||||||
if not hasattr(obj, "get_project"):
|
|
||||||
return
|
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
|
return
|
||||||
|
|
||||||
texts = (getattr(obj, "description", ""),
|
texts = (getattr(obj, "description", ""),
|
||||||
getattr(obj, "content", ""),
|
getattr(obj, "content", ""),
|
||||||
comment,)
|
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
|
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"]:
|
return data.get("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)
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_by_permissions(obj, user):
|
def _filter_by_permissions(obj, user):
|
||||||
|
@ -296,10 +317,11 @@ def send_sync_notifications(notification_id):
|
||||||
msg_id = 'taiga-system'
|
msg_id = 'taiga-system'
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
project_name = remove_lr_cr(notification.project.name)
|
||||||
format_args = {
|
format_args = {
|
||||||
"unsubscribe_url": resolve_front_url('settings-mail-notifications'),
|
"unsubscribe_url": resolve_front_url('settings-mail-notifications'),
|
||||||
"project_slug": notification.project.slug,
|
"project_slug": notification.project.slug,
|
||||||
"project_name": notification.project.name,
|
"project_name": project_name,
|
||||||
"msg_id": msg_id,
|
"msg_id": msg_id,
|
||||||
"time": int(now.timestamp()),
|
"time": int(now.timestamp()),
|
||||||
"domain": domain
|
"domain": domain
|
||||||
|
|
|
@ -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)
|
|
@ -290,6 +290,8 @@ class ProjectSerializer(serializers.LightSerializer):
|
||||||
|
|
||||||
is_fan = Field(attr="is_fan_attr")
|
is_fan = Field(attr="is_fan_attr")
|
||||||
|
|
||||||
|
my_homepage = MethodField()
|
||||||
|
|
||||||
def get_members(self, obj):
|
def get_members(self, obj):
|
||||||
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
|
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
|
||||||
if obj.members_attr is None:
|
if obj.members_attr is None:
|
||||||
|
@ -374,6 +376,13 @@ class ProjectSerializer(serializers.LightSerializer):
|
||||||
def get_logo_big_url(self, obj):
|
def get_logo_big_url(self, obj):
|
||||||
return services.get_logo_big_thumbnail_url(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):
|
class ProjectDetailSerializer(ProjectSerializer):
|
||||||
epic_statuses = Field(attr="epic_statuses_attr")
|
epic_statuses = Field(attr="epic_statuses_attr")
|
||||||
|
|
|
@ -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)
|
|
@ -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")),
|
||||||
|
)
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
|
@ -16,12 +16,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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': {
|
class UserProjectSettingsPermission(TaigaResourcePermission):
|
||||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
retrieve_perms = IsAuthenticated()
|
||||||
'NAME': 'taiga',
|
create_perms = IsAuthenticated()
|
||||||
'USERNAME': 'postgres',
|
update_perms = IsAuthenticated()
|
||||||
}
|
partial_update_perms = IsAuthenticated()
|
||||||
}
|
destroy_perms = IsAuthenticated()
|
||||||
|
list_perms = IsAuthenticated()
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db.models import F
|
||||||
|
|
||||||
from taiga.projects.notifications.services import create_notify_policy_if_not_exists
|
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()
|
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):
|
# Set project on top on user projects list
|
||||||
if instance.user:
|
membership = apps.get_model("projects", "Membership")
|
||||||
create_notify_policy_if_not_exists(instance.project, instance.user)
|
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
|
## Project attributes
|
||||||
|
|
||||||
def project_post_save(sender, instance, created, **kwargs):
|
def project_post_save(sender, instance, created, **kwargs):
|
||||||
"""
|
"""
|
||||||
Populate new project dependen default data
|
Populate new project dependen default data
|
||||||
|
|
|
@ -30,7 +30,9 @@ from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.milestones.models import Milestone
|
from taiga.projects.milestones.models import Milestone
|
||||||
from taiga.projects.mixins.by_ref import ByRefMixin
|
from taiga.projects.mixins.by_ref import ByRefMixin
|
||||||
from taiga.projects.models import Project, TaskStatus
|
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.occ import OCCResourceMixin
|
||||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||||
from taiga.projects.userstories.models import UserStory
|
from taiga.projects.userstories.models import UserStory
|
||||||
|
@ -45,8 +47,10 @@ from . import validators
|
||||||
from . import utils as tasks_utils
|
from . import utils as tasks_utils
|
||||||
|
|
||||||
|
|
||||||
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class TaskViewSet(AssignedToSignalMixin, OCCResourceMixin, VotedResourceMixin,
|
||||||
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
HistoryResourceMixin, WatchedResourceMixin, ByRefMixin,
|
||||||
|
TaggedResourceMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet):
|
||||||
validator_class = validators.TaskValidator
|
validator_class = validators.TaskValidator
|
||||||
queryset = models.Task.objects.all()
|
queryset = models.Task.objects.all()
|
||||||
permission_classes = (permissions.TaskPermission,)
|
permission_classes = (permissions.TaskPermission,)
|
||||||
|
@ -271,6 +275,23 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
|
|
||||||
return response.Ok(tasks_serialized.data)
|
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):
|
def _bulk_update_order(self, order_field, request, **kwargs):
|
||||||
validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
|
validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
|
||||||
if not validator.is_valid():
|
if not validator.is_valid():
|
||||||
|
|
|
@ -91,3 +91,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, DueDateM
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "({1}) {0}".format(self.ref, self.subject)
|
return "({1}) {0}".format(self.ref, self.subject)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.status is not None and self.status.is_closed
|
||||||
|
|
|
@ -35,6 +35,7 @@ class TaskPermission(TaigaResourcePermission):
|
||||||
csv_perms = AllowAny()
|
csv_perms = AllowAny()
|
||||||
bulk_create_perms = HasProjectPerm('add_task')
|
bulk_create_perms = HasProjectPerm('add_task')
|
||||||
bulk_update_order_perms = HasProjectPerm('modify_task')
|
bulk_update_order_perms = HasProjectPerm('modify_task')
|
||||||
|
bulk_update_milestone_perms = HasProjectPerm('modify_task')
|
||||||
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
upvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
||||||
downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
downvote_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
||||||
watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
watch_perms = IsAuthenticated() & HasProjectPerm('view_tasks')
|
||||||
|
|
|
@ -112,6 +112,39 @@ def snapshot_tasks_in_bulk(bulk_data, user):
|
||||||
pass
|
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
|
# CSV
|
||||||
#####################################################
|
#####################################################
|
||||||
|
|
|
@ -31,9 +31,9 @@ def cached_prev_task(sender, instance, **kwargs):
|
||||||
instance.prev = sender.objects.get(id=instance.id)
|
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):
|
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)
|
_try_to_close_or_open_us_when_create_or_edit_task(instance)
|
||||||
|
|
|
@ -153,3 +153,36 @@ class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator
|
||||||
"if it exists, to the same status, user story and/or milestone."))
|
"if it exists, to the same status, user story and/or milestone."))
|
||||||
|
|
||||||
return attrs
|
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
|
||||||
|
|
|
@ -39,6 +39,7 @@ from taiga.projects.history.services import take_snapshot
|
||||||
from taiga.projects.milestones.models import Milestone
|
from taiga.projects.milestones.models import Milestone
|
||||||
from taiga.projects.mixins.by_ref import ByRefMixin
|
from taiga.projects.mixins.by_ref import ByRefMixin
|
||||||
from taiga.projects.models import Project, UserStoryStatus
|
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 WatchedResourceMixin
|
||||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
|
@ -55,8 +56,10 @@ from . import services
|
||||||
from . import validators
|
from . import validators
|
||||||
|
|
||||||
|
|
||||||
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class UserStoryViewSet(AssignedUsersSignalMixin, OCCResourceMixin,
|
||||||
ByRefMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
VotedResourceMixin, HistoryResourceMixin,
|
||||||
|
WatchedResourceMixin, ByRefMixin, TaggedResourceMixin,
|
||||||
|
BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
validator_class = validators.UserStoryValidator
|
validator_class = validators.UserStoryValidator
|
||||||
queryset = models.UserStory.objects.all()
|
queryset = models.UserStory.objects.all()
|
||||||
permission_classes = (permissions.UserStoryPermission,)
|
permission_classes = (permissions.UserStoryPermission,)
|
||||||
|
|
|
@ -72,7 +72,6 @@ def update_milestone_of_tasks_when_edit_us(sender, instance, created, **kwargs):
|
||||||
def try_to_close_or_open_us_and_milestone_when_create_or_edit_us(sender, instance, created, **kwargs):
|
def try_to_close_or_open_us_and_milestone_when_create_or_edit_us(sender, instance, created, **kwargs):
|
||||||
if instance._importing:
|
if instance._importing:
|
||||||
return
|
return
|
||||||
|
|
||||||
_try_to_close_or_open_us_when_create_or_edit_us(instance)
|
_try_to_close_or_open_us_when_create_or_edit_us(instance)
|
||||||
_try_to_close_or_open_milestone_when_create_or_edit_us(instance)
|
_try_to_close_or_open_milestone_when_create_or_edit_us(instance)
|
||||||
|
|
||||||
|
|
|
@ -593,6 +593,30 @@ def attach_public_projects_same_owner(queryset, user, as_field="public_projects_
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def attach_my_homepage(queryset, user, as_field="my_homepage_attr"):
|
||||||
|
"""Attach a homepage array to each object of the queryset.
|
||||||
|
|
||||||
|
:param queryset: A Django projects queryset object.
|
||||||
|
:param as_field: Attach the settings homepage as an attribute with this name.
|
||||||
|
|
||||||
|
:return: Queryset object with the additional `as_field` field.
|
||||||
|
"""
|
||||||
|
model = queryset.model
|
||||||
|
if user is None or user.is_anonymous():
|
||||||
|
sql = "SELECT '{}'"
|
||||||
|
else:
|
||||||
|
sql = """
|
||||||
|
SELECT homepage
|
||||||
|
FROM settings_userprojectsettings
|
||||||
|
WHERE settings_userprojectsettings.project_id = {tbl}.id AND
|
||||||
|
settings_userprojectsettings.user_id = {user_id}"""
|
||||||
|
|
||||||
|
sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
|
||||||
|
|
||||||
|
queryset = queryset.extra(select={as_field: sql})
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
def attach_extra_info(queryset, user=None):
|
def attach_extra_info(queryset, user=None):
|
||||||
queryset = attach_members(queryset)
|
queryset = attach_members(queryset)
|
||||||
queryset = attach_closed_milestones(queryset)
|
queryset = attach_closed_milestones(queryset)
|
||||||
|
@ -618,5 +642,6 @@ def attach_extra_info(queryset, user=None):
|
||||||
queryset = attach_private_projects_same_owner(queryset, user)
|
queryset = attach_private_projects_same_owner(queryset, user)
|
||||||
queryset = attach_public_projects_same_owner(queryset, user)
|
queryset = attach_public_projects_same_owner(queryset, user)
|
||||||
queryset = attach_milestones(queryset)
|
queryset = attach_milestones(queryset)
|
||||||
|
queryset = attach_my_homepage(queryset, user)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
|
@ -43,10 +43,20 @@ from taiga.userstorage.api import StorageEntriesViewSet
|
||||||
router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage")
|
router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage")
|
||||||
|
|
||||||
|
|
||||||
# Notify policies
|
# Notifications & Notify policies
|
||||||
from taiga.projects.notifications.api import NotifyPolicyViewSet
|
from taiga.projects.notifications.api import NotifyPolicyViewSet
|
||||||
|
from taiga.projects.notifications.api import WebNotificationsViewSet
|
||||||
|
|
||||||
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
|
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
|
||||||
|
router.register(r"web-notifications", WebNotificationsViewSet, base_name="web-notifications")
|
||||||
|
router.register(r"web-notifications/set-as-read", WebNotificationsViewSet, base_name="web-notifications")
|
||||||
|
router.register(r"web-notifications/(?P<resource_id>\d+)/set-as-read", WebNotificationsViewSet, base_name="web-notifications")
|
||||||
|
|
||||||
|
# Project settings
|
||||||
|
from taiga.projects.settings.api import UserProjectSettingsViewSet, SectionsViewSet
|
||||||
|
|
||||||
|
router.register(r"user-project-settings", UserProjectSettingsViewSet, base_name="user-project-settings")
|
||||||
|
router.register(r"sections", SectionsViewSet, base_name="sections")
|
||||||
|
|
||||||
|
|
||||||
# Projects & Selectors
|
# Projects & Selectors
|
||||||
|
|
|
@ -80,7 +80,9 @@ class TimelineViewSet(ReadOnlyListViewSet):
|
||||||
qs = qs.extra(where=[
|
qs = qs.extra(where=[
|
||||||
"""
|
"""
|
||||||
NOT(
|
NOT(
|
||||||
data::text LIKE '%%\"values_diff\": {}%%'
|
(data::text LIKE '%%\"values_diff\": {}%%'
|
||||||
|
OR
|
||||||
|
data->'values_diff'->'attachments'->'new' = '[]')
|
||||||
AND
|
AND
|
||||||
event_type::text = ANY('{issues.issue.change,
|
event_type::text = ANY('{issues.issue.change,
|
||||||
tasks.task.change,
|
tasks.task.change,
|
||||||
|
|
|
@ -626,6 +626,10 @@ def create_issue(**kwargs):
|
||||||
return IssueFactory.create(**defaults)
|
return IssueFactory.create(**defaults)
|
||||||
|
|
||||||
|
|
||||||
|
class Missing:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def create_task(**kwargs):
|
def create_task(**kwargs):
|
||||||
"Create a task and along with its dependencies."
|
"Create a task and along with its dependencies."
|
||||||
owner = kwargs.pop("owner", None)
|
owner = kwargs.pop("owner", None)
|
||||||
|
@ -636,13 +640,23 @@ def create_task(**kwargs):
|
||||||
if project is None:
|
if project is None:
|
||||||
project = ProjectFactory.create(owner=owner)
|
project = ProjectFactory.create(owner=owner)
|
||||||
|
|
||||||
|
status = kwargs.pop("status", None)
|
||||||
|
milestone = kwargs.pop("milestone", None)
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"project": project,
|
"project": project,
|
||||||
"owner": owner,
|
"owner": owner,
|
||||||
"status": TaskStatusFactory.create(project=project),
|
"status": status or TaskStatusFactory.create(project=project),
|
||||||
"milestone": MilestoneFactory.create(project=project),
|
"milestone": milestone or MilestoneFactory.create(project=project),
|
||||||
"user_story": UserStoryFactory.create(project=project, owner=owner),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user_story = kwargs.pop("user_story", Missing)
|
||||||
|
|
||||||
|
defaults["user_story"] = (
|
||||||
|
UserStoryFactory.create(project=project, owner=owner, milestone=defaults["milestone"])
|
||||||
|
if user_story is Missing
|
||||||
|
else user_story
|
||||||
|
)
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
|
|
||||||
return TaskFactory.create(**defaults)
|
return TaskFactory.create(**defaults)
|
||||||
|
|
|
@ -79,6 +79,9 @@ def data():
|
||||||
default_points=m.private_points1)
|
default_points=m.private_points1)
|
||||||
m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
|
m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
|
||||||
|
|
||||||
|
m.private_sprint1 = f.MilestoneFactory.create(project=m.private_project1,
|
||||||
|
owner=m.project_owner)
|
||||||
|
|
||||||
m.private_points2 = f.PointsFactory()
|
m.private_points2 = f.PointsFactory()
|
||||||
m.private_project2 = f.ProjectFactory(is_private=True,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
|
@ -697,6 +700,28 @@ def test_user_story_action_bulk_update_order(client, data):
|
||||||
assert results == [401, 403, 403, 451, 451]
|
assert results == [401, 403, 403, 451, 451]
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_story_action_bulk_update_milestone(client, data):
|
||||||
|
url = reverse('userstories-bulk-update-milestone')
|
||||||
|
|
||||||
|
users = [
|
||||||
|
None,
|
||||||
|
data.registered_user,
|
||||||
|
data.project_member_without_perms,
|
||||||
|
data.project_member_with_perms,
|
||||||
|
data.project_owner
|
||||||
|
]
|
||||||
|
|
||||||
|
post_data = json.dumps({
|
||||||
|
"bulk_stories": [
|
||||||
|
{"us_id": data.private_user_story1.id, "order": 2}
|
||||||
|
],
|
||||||
|
"milestone_id": data.private_sprint1.pk,
|
||||||
|
"project_id": data.private_project1.pk
|
||||||
|
})
|
||||||
|
results = helper_test_http_method(client, 'post', url, post_data, users)
|
||||||
|
assert results == [401, 403, 403, 204, 204]
|
||||||
|
|
||||||
|
|
||||||
def test_user_story_action_upvote(client, data):
|
def test_user_story_action_upvote(client, data):
|
||||||
public_url = reverse('userstories-upvote', kwargs={"pk": data.public_user_story.pk})
|
public_url = reverse('userstories-upvote', kwargs={"pk": data.public_user_story.pk})
|
||||||
private_url1 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story1.pk})
|
private_url1 = reverse('userstories-upvote', kwargs={"pk": data.private_user_story1.pk})
|
||||||
|
|
|
@ -39,6 +39,78 @@ import pytest
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def create_filter_issues_context():
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
data["project"] = f.ProjectFactory.create()
|
||||||
|
project = data["project"]
|
||||||
|
data["users"] = [f.UserFactory.create(is_superuser=True) for i in range(0, 3)]
|
||||||
|
data["roles"] = [f.RoleFactory.create() for i in range(0, 3)]
|
||||||
|
user_roles = zip(data["users"], data["roles"])
|
||||||
|
# Add membership fixtures
|
||||||
|
[f.MembershipFactory.create(user=user, project=project, role=role) for (user, role) in user_roles]
|
||||||
|
|
||||||
|
data["statuses"] = [f.IssueStatusFactory.create(project=project) for i in range(0, 4)]
|
||||||
|
data["types"] = [f.IssueTypeFactory.create(project=project) for i in range(0, 2)]
|
||||||
|
data["severities"] = [f.SeverityFactory.create(project=project) for i in range(0, 4)]
|
||||||
|
data["priorities"] = [f.PriorityFactory.create(project=project) for i in range(0, 4)]
|
||||||
|
data["tags"] = ["test1test2test3", "test1", "test2", "test3"]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# | Issue | Owner | Assigned To | Status | Type | Priority | Severity | Tags |
|
||||||
|
# |-------#--------#-------------#---------#-------#-----------#-----------#---------------------|
|
||||||
|
# | 0 | user2 | None | status3 | type1 | priority2 | severity1 | tag1 |
|
||||||
|
# | 1 | user1 | None | status3 | type2 | priority2 | severity1 | tag2 |
|
||||||
|
# | 2 | user3 | None | status1 | type1 | priority3 | severity2 | tag1 tag2 |
|
||||||
|
# | 3 | user2 | None | status0 | type2 | priority3 | severity1 | tag3 |
|
||||||
|
# | 4 | user1 | user1 | status0 | type1 | priority2 | severity3 | tag1 tag2 tag3 |
|
||||||
|
# | 5 | user3 | user1 | status2 | type2 | priority3 | severity2 | tag3 |
|
||||||
|
# | 6 | user2 | user1 | status3 | type1 | priority2 | severity0 | tag1 tag2 |
|
||||||
|
# | 7 | user1 | user2 | status0 | type2 | priority1 | severity3 | tag3 |
|
||||||
|
# | 8 | user3 | user2 | status3 | type1 | priority0 | severity1 | tag1 |
|
||||||
|
# | 9 | user2 | user3 | status1 | type2 | priority0 | severity2 | tag0 |
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
(user1, user2, user3, ) = data["users"]
|
||||||
|
(status0, status1, status2, status3 ) = data["statuses"]
|
||||||
|
(type1, type2, ) = data["types"]
|
||||||
|
(severity0, severity1, severity2, severity3, ) = data["severities"]
|
||||||
|
(priority0, priority1, priority2, priority3, ) = data["priorities"]
|
||||||
|
(tag0, tag1, tag2, tag3, ) = data["tags"]
|
||||||
|
|
||||||
|
f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
|
status=status3, type=type1, priority=priority2, severity=severity1,
|
||||||
|
tags=[tag1])
|
||||||
|
f.IssueFactory.create(project=project, owner=user1, assigned_to=None,
|
||||||
|
status=status3, type=type2, priority=priority2, severity=severity1,
|
||||||
|
tags=[tag2])
|
||||||
|
f.IssueFactory.create(project=project, owner=user3, assigned_to=None,
|
||||||
|
status=status1, type=type1, priority=priority3, severity=severity2,
|
||||||
|
tags=[tag1, tag2])
|
||||||
|
f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
|
status=status0, type=type2, priority=priority3, severity=severity1,
|
||||||
|
tags=[tag3])
|
||||||
|
f.IssueFactory.create(project=project, owner=user1, assigned_to=user1,
|
||||||
|
status=status0, type=type1, priority=priority2, severity=severity3,
|
||||||
|
tags=[tag1, tag2, tag3])
|
||||||
|
f.IssueFactory.create(project=project, owner=user3, assigned_to=user1,
|
||||||
|
status=status2, type=type2, priority=priority3, severity=severity2,
|
||||||
|
tags=[tag3])
|
||||||
|
f.IssueFactory.create(project=project, owner=user2, assigned_to=user1,
|
||||||
|
status=status3, type=type1, priority=priority2, severity=severity0,
|
||||||
|
tags=[tag1, tag2])
|
||||||
|
f.IssueFactory.create(project=project, owner=user1, assigned_to=user2,
|
||||||
|
status=status0, type=type2, priority=priority1, severity=severity3,
|
||||||
|
tags=[tag3])
|
||||||
|
f.IssueFactory.create(project=project, owner=user3, assigned_to=user2,
|
||||||
|
status=status3, type=type1, priority=priority0, severity=severity1,
|
||||||
|
tags=[tag1])
|
||||||
|
f.IssueFactory.create(project=project, owner=user2, assigned_to=user3,
|
||||||
|
status=status1, type=type2, priority=priority0, severity=severity2,
|
||||||
|
tags=[tag0])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def test_get_issues_from_bulk():
|
def test_get_issues_from_bulk():
|
||||||
data = """
|
data = """
|
||||||
Issue #1
|
Issue #1
|
||||||
|
@ -370,86 +442,63 @@ def test_api_filter_by_finished_date(client):
|
||||||
assert response.data[0]["ref"] == finished_issue.ref
|
assert response.data[0]["ref"] == finished_issue.ref
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("filter_name,collection,expected,exclude_expected,is_text", [
|
||||||
|
('type', 'types', 5, 5, False),
|
||||||
|
('severity', 'severities', 1, 9, False),
|
||||||
|
('priority', 'priorities', 2, 8, False),
|
||||||
|
('status', 'statuses', 3, 7, False),
|
||||||
|
('assigned_to', 'users', 3, 7, False),
|
||||||
|
('tags', 'tags', 1, 9, True),
|
||||||
|
('owner', 'users', 3, 7, False),
|
||||||
|
('role', 'roles', 3, 7, False),
|
||||||
|
])
|
||||||
|
def test_api_filters(client, filter_name, collection, expected, exclude_expected, is_text):
|
||||||
|
data = create_filter_issues_context()
|
||||||
|
project = data["project"]
|
||||||
|
options = data[collection]
|
||||||
|
|
||||||
|
client.login(data["users"][0])
|
||||||
|
if is_text:
|
||||||
|
param = options[0]
|
||||||
|
else:
|
||||||
|
param = options[0].id
|
||||||
|
|
||||||
|
# include test
|
||||||
|
url = "{}?project={}&{}={}".format(reverse('issues-list'), project.id, filter_name, param)
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == expected
|
||||||
|
|
||||||
|
# exclude test
|
||||||
|
url = "{}?project={}&exclude_{}={}".format(reverse('issues-list'), project.id, filter_name, param)
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == exclude_expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_mulitple_exclude_filter_tags(client):
|
||||||
|
data = create_filter_issues_context()
|
||||||
|
project = data["project"]
|
||||||
|
client.login(data["users"][0])
|
||||||
|
tags = data["tags"]
|
||||||
|
|
||||||
|
url = "{}?project={}&exclude_tags={},{}".format(reverse('issues-list'), project.id, tags[1], tags[2])
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == 4
|
||||||
|
|
||||||
|
|
||||||
def test_api_filters_data(client):
|
def test_api_filters_data(client):
|
||||||
project = f.ProjectFactory.create()
|
data = create_filter_issues_context()
|
||||||
user1 = f.UserFactory.create(is_superuser=True)
|
project = data["project"]
|
||||||
f.MembershipFactory.create(user=user1, project=project)
|
(user1, user2, user3, ) = data["users"]
|
||||||
user2 = f.UserFactory.create(is_superuser=True)
|
(status0, status1, status2, status3, ) = data["statuses"]
|
||||||
f.MembershipFactory.create(user=user2, project=project)
|
(type1, type2, ) = data["types"]
|
||||||
user3 = f.UserFactory.create(is_superuser=True)
|
(priority0, priority1, priority2, priority3, ) = data["priorities"]
|
||||||
f.MembershipFactory.create(user=user3, project=project)
|
(severity0, severity1, severity2, severity3, ) = data["severities"]
|
||||||
|
(tag0, tag1, tag2, tag3, ) = data["tags"]
|
||||||
status0 = f.IssueStatusFactory.create(project=project)
|
|
||||||
status1 = f.IssueStatusFactory.create(project=project)
|
|
||||||
status2 = f.IssueStatusFactory.create(project=project)
|
|
||||||
status3 = f.IssueStatusFactory.create(project=project)
|
|
||||||
|
|
||||||
type1 = f.IssueTypeFactory.create(project=project)
|
|
||||||
type2 = f.IssueTypeFactory.create(project=project)
|
|
||||||
|
|
||||||
severity0 = f.SeverityFactory.create(project=project)
|
|
||||||
severity1 = f.SeverityFactory.create(project=project)
|
|
||||||
severity2 = f.SeverityFactory.create(project=project)
|
|
||||||
severity3 = f.SeverityFactory.create(project=project)
|
|
||||||
|
|
||||||
priority0 = f.PriorityFactory.create(project=project)
|
|
||||||
priority1 = f.PriorityFactory.create(project=project)
|
|
||||||
priority2 = f.PriorityFactory.create(project=project)
|
|
||||||
priority3 = f.PriorityFactory.create(project=project)
|
|
||||||
|
|
||||||
tag0 = "test1test2test3"
|
|
||||||
tag1 = "test1"
|
|
||||||
tag2 = "test2"
|
|
||||||
tag3 = "test3"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------
|
|
||||||
# | Issue | Owner | Assigned To | Status | Type | Priority | Severity | Tags |
|
|
||||||
# |-------#--------#-------------#---------#-------#-----------#-----------#---------------------|
|
|
||||||
# | 0 | user2 | None | status3 | type1 | priority2 | severity1 | tag1 |
|
|
||||||
# | 1 | user1 | None | status3 | type2 | priority2 | severity1 | tag2 |
|
|
||||||
# | 2 | user3 | None | status1 | type1 | priority3 | severity2 | tag1 tag2 |
|
|
||||||
# | 3 | user2 | None | status0 | type2 | priority3 | severity1 | tag3 |
|
|
||||||
# | 4 | user1 | user1 | status0 | type1 | priority2 | severity3 | tag1 tag2 tag3 |
|
|
||||||
# | 5 | user3 | user1 | status2 | type2 | priority3 | severity2 | tag3 |
|
|
||||||
# | 6 | user2 | user1 | status3 | type1 | priority2 | severity0 | tag1 tag2 |
|
|
||||||
# | 7 | user1 | user2 | status0 | type2 | priority1 | severity3 | tag3 |
|
|
||||||
# | 8 | user3 | user2 | status3 | type1 | priority0 | severity1 | tag1 |
|
|
||||||
# | 9 | user2 | user3 | status1 | type2 | priority0 | severity2 | tag0 |
|
|
||||||
# ------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
issue0 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
|
|
||||||
status=status3, type=type1, priority=priority2, severity=severity1,
|
|
||||||
tags=[tag1])
|
|
||||||
issue1 = f.IssueFactory.create(project=project, owner=user1, assigned_to=None,
|
|
||||||
status=status3, type=type2, priority=priority2, severity=severity1,
|
|
||||||
tags=[tag2])
|
|
||||||
issue2 = f.IssueFactory.create(project=project, owner=user3, assigned_to=None,
|
|
||||||
status=status1, type=type1, priority=priority3, severity=severity2,
|
|
||||||
tags=[tag1, tag2])
|
|
||||||
issue3 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None,
|
|
||||||
status=status0, type=type2, priority=priority3, severity=severity1,
|
|
||||||
tags=[tag3])
|
|
||||||
issue4 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user1,
|
|
||||||
status=status0, type=type1, priority=priority2, severity=severity3,
|
|
||||||
tags=[tag1, tag2, tag3])
|
|
||||||
issue5 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user1,
|
|
||||||
status=status2, type=type2, priority=priority3, severity=severity2,
|
|
||||||
tags=[tag3])
|
|
||||||
issue6 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user1,
|
|
||||||
status=status3, type=type1, priority=priority2, severity=severity0,
|
|
||||||
tags=[tag1, tag2])
|
|
||||||
issue7 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user2,
|
|
||||||
status=status0, type=type2, priority=priority1, severity=severity3,
|
|
||||||
tags=[tag3])
|
|
||||||
issue8 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user2,
|
|
||||||
status=status3, type=type1, priority=priority0, severity=severity1,
|
|
||||||
tags=[tag1])
|
|
||||||
issue9 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user3,
|
|
||||||
status=status1, type=type2, priority=priority0, severity=severity2,
|
|
||||||
tags=[tag0])
|
|
||||||
|
|
||||||
url = reverse("issues-filters-data") + "?project={}".format(project.id)
|
url = reverse("issues-filters-data") + "?project={}".format(project.id)
|
||||||
|
|
||||||
client.login(user1)
|
client.login(user1)
|
||||||
|
|
||||||
## No filter
|
## No filter
|
||||||
|
@ -758,6 +807,69 @@ def test_api_create_in_bulk_with_status_milestone(client):
|
||||||
assert response.data[0]["milestone"] == milestone.id
|
assert response.data[0]["milestone"] == milestone.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_update_milestone_in_bulk(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
|
||||||
|
|
||||||
|
milestone1 = f.MilestoneFactory(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory(project=project)
|
||||||
|
|
||||||
|
i1 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
i2 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
i3 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).issues.count() == 3
|
||||||
|
|
||||||
|
url = reverse("issues-bulk-update-milestone")
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_issues": [
|
||||||
|
{"issue_id": i1.id},
|
||||||
|
{"issue_id": i2.id},
|
||||||
|
{"issue_id": i3.id}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
assert response.data[i1.id] == milestone2.id
|
||||||
|
assert response.data[i2.id] == milestone2.id
|
||||||
|
assert response.data[i3.id] == milestone2.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_update_milestone_in_bulk_invalid_milestone(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
|
||||||
|
|
||||||
|
milestone1 = f.MilestoneFactory(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory()
|
||||||
|
|
||||||
|
i1 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
i2 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
i3 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
url = reverse("issues-bulk-update-milestone")
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_issues": [
|
||||||
|
{"issue_id": i1.id},
|
||||||
|
{"issue_id": i2.id},
|
||||||
|
{"issue_id": i3.id}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "milestone_id" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_get_issues(client):
|
def test_get_issues(client):
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
|
|
|
@ -26,7 +26,6 @@ from urllib.parse import quote
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.projects.userstories.serializers import UserStorySerializer
|
|
||||||
|
|
||||||
from .. import factories as f
|
from .. import factories as f
|
||||||
|
|
||||||
|
@ -180,3 +179,211 @@ def test_api_filter_by_milestone__estimated_start_and_end(client, field_name):
|
||||||
assert number_of_milestones == expection, param
|
assert number_of_milestones == expection, param
|
||||||
if number_of_milestones > 0:
|
if number_of_milestones > 0:
|
||||||
assert response.data[0]["slug"] == milestone.slug
|
assert response.data[0]["slug"] == milestone.slug
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_update_milestone_in_bulk_userstories(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
us1 = f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
sprint_order=1)
|
||||||
|
us2 = f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
sprint_order=2)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).user_stories.count() == 2
|
||||||
|
|
||||||
|
url = reverse("milestones-move-userstories-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_stories": [{"us_id": us2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).user_stories.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).user_stories.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_move_userstories_to_another_sprint(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
us1 = f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
sprint_order=1)
|
||||||
|
us2 = f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
sprint_order=2)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).user_stories.count() == 2
|
||||||
|
|
||||||
|
url = reverse("milestones-move-userstories-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_stories": [{"us_id": us2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).user_stories.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).user_stories.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_move_userstories_to_another_sprint_close_previous(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.UserStoryStatusFactory.create(is_closed=True)
|
||||||
|
us1 = f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
sprint_order=1, status=closed_status)
|
||||||
|
us2 = f.create_userstory(project=project, milestone=milestone1, sprint_order=2)
|
||||||
|
|
||||||
|
assert milestone1.user_stories.count() == 2
|
||||||
|
assert not milestone1.closed
|
||||||
|
|
||||||
|
url = reverse("milestones-move-userstories-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_stories": [{"us_id": us2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).user_stories.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).user_stories.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone1.id).closed
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_move_tasks_to_another_sprint(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
task1 = f.create_task(project=project, milestone=milestone1, taskboard_order=1)
|
||||||
|
task2 = f.create_task(project=project, milestone=milestone1, taskboard_order=2)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).tasks.count() == 2
|
||||||
|
|
||||||
|
url = reverse("milestones-move-tasks-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_tasks": [{"task_id": task2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).tasks.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).tasks.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_move_tasks_to_another_sprint_close_previous(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.TaskStatusFactory.create(project=project, is_closed=True)
|
||||||
|
|
||||||
|
task1 = f.create_task(project=project, milestone=milestone1, taskboard_order=1,
|
||||||
|
status=closed_status, user_story=None)
|
||||||
|
task2 = f.create_task(project=project, milestone=milestone1, taskboard_order=2,
|
||||||
|
user_story=None)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).tasks.count() == 2
|
||||||
|
assert not milestone1.closed
|
||||||
|
|
||||||
|
url = reverse("milestones-move-tasks-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_tasks": [{"task_id": task2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).tasks.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).tasks.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone1.id).closed
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_move_issues_to_another_sprint(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
issue1 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
issue2 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).issues.count() == 2
|
||||||
|
|
||||||
|
url = reverse("milestones-move-issues-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_issues": [{"issue_id": issue2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).issues.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).issues.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_move_issues_to_another_sprint_close_previous(client):
|
||||||
|
project = f.create_project()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
issue1 = f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
issue2 = f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert project.milestones.get(id=milestone1.id).closed is False
|
||||||
|
assert project.milestones.get(id=milestone1.id).issues.count() == 2
|
||||||
|
|
||||||
|
url = reverse("milestones-move-issues-to-sprint", kwargs={"pk": milestone1.pk})
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_issues": [{"issue_id": issue2.id, "order": 2}]
|
||||||
|
}
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 204, response.data
|
||||||
|
assert project.milestones.get(id=milestone1.id).issues.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone2.id).issues.count() == 1
|
||||||
|
assert project.milestones.get(id=milestone1.id).closed
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import time
|
import time
|
||||||
import math
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -35,17 +34,16 @@ from django.utils import timezone
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from .. import factories as f
|
from .. import factories as f
|
||||||
|
|
||||||
|
from taiga.base.api.settings import api_settings
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.projects.notifications import services
|
from taiga.projects.notifications import services
|
||||||
from taiga.projects.notifications import utils
|
|
||||||
from taiga.projects.notifications import models
|
from taiga.projects.notifications import models
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
|
from taiga.projects.notifications.choices import WebNotificationType
|
||||||
from taiga.projects.history.choices import HistoryType
|
from taiga.projects.history.choices import HistoryType
|
||||||
from taiga.projects.history.services import take_snapshot
|
from taiga.projects.history.services import take_snapshot
|
||||||
from taiga.projects.issues.serializers import IssueSerializer
|
|
||||||
from taiga.projects.userstories.serializers import UserStorySerializer
|
|
||||||
from taiga.projects.tasks.serializers import TaskSerializer
|
|
||||||
from taiga.permissions.choices import MEMBERS_PERMISSIONS
|
from taiga.permissions.choices import MEMBERS_PERMISSIONS
|
||||||
|
from taiga.users.gravatar import get_user_gravatar_id
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
@ -57,6 +55,20 @@ def mail():
|
||||||
return mail
|
return mail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"header, expected",
|
||||||
|
[
|
||||||
|
("", ""),
|
||||||
|
("One line", "One line"),
|
||||||
|
("Two \nlines", "Two lines"),
|
||||||
|
("Mix \r\nCR and LF \rin the string", "Mix CR and LF in the string"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_remove_lr_cr(header, expected):
|
||||||
|
rv = services.remove_lr_cr(header)
|
||||||
|
assert rv == expected
|
||||||
|
|
||||||
|
|
||||||
def test_create_retrieve_notify_policy():
|
def test_create_retrieve_notify_policy():
|
||||||
project = f.ProjectFactory.create()
|
project = f.ProjectFactory.create()
|
||||||
|
|
||||||
|
@ -1074,3 +1086,340 @@ def parse_ms_thread_index(index):
|
||||||
ts.append(ts[-1] + datetime.timedelta(microseconds=(f << 18)//10))
|
ts.append(ts[-1] + datetime.timedelta(microseconds=(f << 18)//10))
|
||||||
|
|
||||||
return guid, ts
|
return guid, ts
|
||||||
|
|
||||||
|
|
||||||
|
def _notification_data(project, user, obj, content_type):
|
||||||
|
return {
|
||||||
|
"project": {
|
||||||
|
"id": project.pk,
|
||||||
|
"slug": project.slug,
|
||||||
|
"name": project.name,
|
||||||
|
},
|
||||||
|
"obj": {
|
||||||
|
"id": obj.pk,
|
||||||
|
"ref": obj.ref,
|
||||||
|
"subject": obj.subject,
|
||||||
|
"content_type": content_type,
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
'big_photo': None,
|
||||||
|
'date_joined': user.date_joined.strftime(
|
||||||
|
api_settings.DATETIME_FORMAT),
|
||||||
|
'gravatar_id': get_user_gravatar_id(user),
|
||||||
|
'id': user.pk,
|
||||||
|
'is_profile_visible': True,
|
||||||
|
'name': user.get_full_name(),
|
||||||
|
'photo': None,
|
||||||
|
'username': user.username
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_issue_updated_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(
|
||||||
|
project=project,
|
||||||
|
permissions=['view_issues', 'modify_issue']
|
||||||
|
)
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member3 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member4 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
issue = f.IssueFactory.create(project=project, owner=member1.user)
|
||||||
|
|
||||||
|
client.login(member1.user)
|
||||||
|
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
|
||||||
|
with patch(mock_path):
|
||||||
|
client.patch(
|
||||||
|
reverse("issues-detail", args=[issue.pk]),
|
||||||
|
json.dumps({
|
||||||
|
"description": "Lorem ipsum @%s dolor sit amet" %
|
||||||
|
member4.user.username,
|
||||||
|
"assigned_to": member2.user.pk,
|
||||||
|
"watchers": [member3.user.pk],
|
||||||
|
"version": issue.version
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 3 == models.WebNotification.objects.count()
|
||||||
|
|
||||||
|
notifications = models.WebNotification.objects.all()
|
||||||
|
notification_data = _notification_data(project, member1.user, issue,
|
||||||
|
'issue')
|
||||||
|
# Notification assigned_to
|
||||||
|
assert notifications[0].user == member2.user
|
||||||
|
assert notifications[0].event_type == WebNotificationType.assigned.value
|
||||||
|
assert notifications[0].read is None
|
||||||
|
assert notifications[0].data == notification_data
|
||||||
|
|
||||||
|
# Notification added_as_watcher
|
||||||
|
assert notifications[1].user == member3.user
|
||||||
|
assert notifications[1].event_type == WebNotificationType.added_as_watcher
|
||||||
|
assert notifications[1].read is None
|
||||||
|
assert notifications[1].data == notification_data
|
||||||
|
|
||||||
|
# Notification mentioned
|
||||||
|
assert notifications[2].user == member4.user
|
||||||
|
assert notifications[2].event_type == WebNotificationType.mentioned
|
||||||
|
assert notifications[2].read is None
|
||||||
|
assert notifications[2].data == notification_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_on_issue_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(
|
||||||
|
project=project,
|
||||||
|
permissions=['view_issues', 'modify_issue']
|
||||||
|
)
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
issue = f.IssueFactory.create(project=project, owner=member1.user)
|
||||||
|
issue.add_watcher(member2.user)
|
||||||
|
|
||||||
|
client.login(member1.user)
|
||||||
|
mock_path = "taiga.projects.issues.api.IssueViewSet.pre_conditions_on_save"
|
||||||
|
with patch(mock_path):
|
||||||
|
client.patch(
|
||||||
|
reverse("issues-detail", args=[issue.pk]),
|
||||||
|
json.dumps({
|
||||||
|
"version": issue.version,
|
||||||
|
"comment": "Lorem ipsum dolor sit amet",
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 1 == models.WebNotification.objects.count()
|
||||||
|
|
||||||
|
notification = models.WebNotification.objects.first()
|
||||||
|
notification_data = _notification_data(project, member1.user, issue,
|
||||||
|
'issue')
|
||||||
|
|
||||||
|
# Notification comment
|
||||||
|
assert notification.user == member2.user
|
||||||
|
assert notification.event_type == WebNotificationType.comment
|
||||||
|
assert notification.read is None
|
||||||
|
assert notification.data == notification_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_task_updated_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(
|
||||||
|
project=project,
|
||||||
|
permissions=['view_tasks', 'modify_task']
|
||||||
|
)
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member3 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member4 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
task = f.TaskFactory.create(project=project, owner=member1.user)
|
||||||
|
|
||||||
|
client.login(member1.user)
|
||||||
|
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
|
||||||
|
with patch(mock_path):
|
||||||
|
client.patch(
|
||||||
|
reverse("tasks-detail", args=[task.pk]),
|
||||||
|
json.dumps({
|
||||||
|
"description": "Lorem ipsum @%s dolor sit amet" %
|
||||||
|
member4.user.username,
|
||||||
|
"assigned_to": member2.user.pk,
|
||||||
|
"watchers": [member3.user.pk],
|
||||||
|
"version": task.version
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 3 == models.WebNotification.objects.count()
|
||||||
|
|
||||||
|
notifications = models.WebNotification.objects.all()
|
||||||
|
notification_data = _notification_data(project, member1.user, task, 'task')
|
||||||
|
|
||||||
|
# Notification assigned_to
|
||||||
|
assert notifications[0].user == member2.user
|
||||||
|
assert notifications[0].event_type == WebNotificationType.assigned.value
|
||||||
|
assert notifications[0].read is None
|
||||||
|
assert notifications[0].data == notification_data
|
||||||
|
|
||||||
|
# Notification added_as_watcher
|
||||||
|
assert notifications[1].user == member3.user
|
||||||
|
assert notifications[1].event_type == WebNotificationType.added_as_watcher
|
||||||
|
assert notifications[1].read is None
|
||||||
|
assert notifications[1].data == notification_data
|
||||||
|
|
||||||
|
# Notification mentioned
|
||||||
|
assert notifications[2].user == member4.user
|
||||||
|
assert notifications[2].event_type == WebNotificationType.mentioned
|
||||||
|
assert notifications[2].read is None
|
||||||
|
assert notifications[2].data == notification_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_on_task_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(
|
||||||
|
project=project,
|
||||||
|
permissions=['view_tasks', 'modify_task']
|
||||||
|
)
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
task = f.TaskFactory.create(project=project, owner=member1.user)
|
||||||
|
task.add_watcher(member2.user)
|
||||||
|
|
||||||
|
client.login(member1.user)
|
||||||
|
mock_path = "taiga.projects.tasks.api.TaskViewSet.pre_conditions_on_save"
|
||||||
|
with patch(mock_path):
|
||||||
|
client.patch(
|
||||||
|
reverse("tasks-detail", args=[task.pk]),
|
||||||
|
json.dumps({
|
||||||
|
"version": task.version,
|
||||||
|
"comment": "Lorem ipsum dolor sit amet",
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 1 == models.WebNotification.objects.count()
|
||||||
|
|
||||||
|
notification = models.WebNotification.objects.first()
|
||||||
|
notification_data = _notification_data(project, member1.user, task, 'task')
|
||||||
|
|
||||||
|
# Notification comment
|
||||||
|
assert notification.user == member2.user
|
||||||
|
assert notification.event_type == WebNotificationType.comment
|
||||||
|
assert notification.read is None
|
||||||
|
assert notification.data == notification_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_us_updated_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(
|
||||||
|
project=project,
|
||||||
|
permissions=['view_us', 'modify_us']
|
||||||
|
)
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member3 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member4 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
us = f.UserStoryFactory.create(project=project,
|
||||||
|
owner=member1.user,
|
||||||
|
milestone=None)
|
||||||
|
|
||||||
|
client.login(member1.user)
|
||||||
|
mock_path = "taiga.projects.userstories.api.UserStoryViewSet." \
|
||||||
|
"pre_conditions_on_save"
|
||||||
|
with patch(mock_path):
|
||||||
|
client.patch(
|
||||||
|
reverse("userstories-detail", args=[us.pk]),
|
||||||
|
json.dumps({
|
||||||
|
"description": "Lorem ipsum @%s dolor sit amet" %
|
||||||
|
member4.user.username,
|
||||||
|
"assigned_users": [member2.user.pk],
|
||||||
|
"watchers": [member3.user.pk],
|
||||||
|
"version": us.version
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 3 == models.WebNotification.objects.count()
|
||||||
|
|
||||||
|
notifications = models.WebNotification.objects.all()
|
||||||
|
notification_data = _notification_data(project, member1.user, us,
|
||||||
|
'userstory')
|
||||||
|
|
||||||
|
# Notification added_as_watcher
|
||||||
|
assert notifications[0].user == member3.user
|
||||||
|
assert notifications[0].event_type == WebNotificationType.added_as_watcher
|
||||||
|
assert notifications[0].read is None
|
||||||
|
assert notifications[0].data == notification_data
|
||||||
|
|
||||||
|
# Notification mentioned
|
||||||
|
assert notifications[1].user == member4.user
|
||||||
|
assert notifications[1].event_type == WebNotificationType.mentioned
|
||||||
|
assert notifications[1].read is None
|
||||||
|
assert notifications[1].data == notification_data
|
||||||
|
|
||||||
|
# Notification assigned_users
|
||||||
|
assert notifications[2].user == member2.user
|
||||||
|
assert notifications[2].event_type == WebNotificationType.assigned.value
|
||||||
|
assert notifications[2].read is None
|
||||||
|
assert notifications[2].data == notification_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_on_us_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
role = f.RoleFactory.create(
|
||||||
|
project=project,
|
||||||
|
permissions=['view_us', 'modify_us']
|
||||||
|
)
|
||||||
|
member1 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
member2 = f.MembershipFactory.create(project=project, role=role)
|
||||||
|
us = f.UserStoryFactory.create(project=project,
|
||||||
|
owner=member1.user,
|
||||||
|
milestone=None)
|
||||||
|
us.add_watcher(member2.user)
|
||||||
|
|
||||||
|
client.login(member1.user)
|
||||||
|
mock_path = "taiga.projects.userstories.api.UserStoryViewSet." \
|
||||||
|
"pre_conditions_on_save"
|
||||||
|
with patch(mock_path):
|
||||||
|
client.patch(
|
||||||
|
reverse("userstories-detail", args=[us.pk]),
|
||||||
|
json.dumps({
|
||||||
|
"version": us.version,
|
||||||
|
"comment": "Lorem ipsum dolor sit amet",
|
||||||
|
}),
|
||||||
|
content_type="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 1 == models.WebNotification.objects.count()
|
||||||
|
|
||||||
|
notification = models.WebNotification.objects.first()
|
||||||
|
notification_data = _notification_data(project, member1.user, us,
|
||||||
|
'userstory')
|
||||||
|
|
||||||
|
# Notification comment
|
||||||
|
assert notification.user == member2.user
|
||||||
|
assert notification.event_type == WebNotificationType.comment
|
||||||
|
assert notification.read is None
|
||||||
|
assert notification.data == notification_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_member_generates_web_notifications(client):
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
john = f.UserFactory.create()
|
||||||
|
joseph = f.UserFactory.create()
|
||||||
|
other = f.UserFactory.create()
|
||||||
|
tester = f.RoleFactory(project=project, name="Tester",
|
||||||
|
permissions=["view_project"])
|
||||||
|
gamer = f.RoleFactory(project=project, name="Gamer",
|
||||||
|
permissions=["view_project"])
|
||||||
|
f.MembershipFactory(project=project, user=john, role=tester, is_admin=True)
|
||||||
|
|
||||||
|
# John and Other are members from another project
|
||||||
|
project2 = f.ProjectFactory()
|
||||||
|
f.MembershipFactory(project=project2, user=john, role=gamer, is_admin=True)
|
||||||
|
f.MembershipFactory(project=project2, user=other, role=gamer)
|
||||||
|
|
||||||
|
url = reverse("memberships-bulk-create")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"bulk_memberships": [
|
||||||
|
{"role_id": gamer.pk, "username": joseph.email},
|
||||||
|
{"role_id": gamer.pk, "username": other.username},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
client.login(john)
|
||||||
|
client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert models.WebNotification.objects.count() == 2
|
||||||
|
|
||||||
|
notifications = models.WebNotification.objects.all()
|
||||||
|
|
||||||
|
# Notification added_as_member
|
||||||
|
assert notifications[0].user == joseph
|
||||||
|
assert notifications[0].event_type == WebNotificationType.added_as_member
|
||||||
|
assert notifications[0].read is None
|
||||||
|
|
||||||
|
# Notification added_as_member
|
||||||
|
assert notifications[1].user == other
|
||||||
|
assert notifications[1].event_type == WebNotificationType.added_as_member
|
||||||
|
assert notifications[1].read is None
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
|
||||||
|
from taiga.projects.settings import services
|
||||||
|
from taiga.projects.settings.choices import Section
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_page_setting_existence():
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
assert not services.user_project_settings_exists(project, project.owner)
|
||||||
|
|
||||||
|
services.create_user_project_settings(project, project.owner, Section.kanban)
|
||||||
|
assert services.user_project_settings_exists(project, project.owner)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_retrieve_home_page_setting():
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
|
||||||
|
policy_model_cls = apps.get_model("settings", "UserProjectSettings")
|
||||||
|
current_number = policy_model_cls.objects.all().count()
|
||||||
|
assert current_number == 0
|
||||||
|
|
||||||
|
setting = services.create_user_project_settings_if_not_exists(project,
|
||||||
|
project.owner)
|
||||||
|
|
||||||
|
current_number = policy_model_cls.objects.all().count()
|
||||||
|
assert current_number == 1
|
||||||
|
assert setting.homepage == Section.timeline
|
||||||
|
|
||||||
|
|
||||||
|
def test_retrieve_homepage_setting_with_allowed_sections(client):
|
||||||
|
# Default template has next configuration:
|
||||||
|
# "is_epics_activated": false,
|
||||||
|
# "is_backlog_activated": true,
|
||||||
|
# "is_kanban_activated": false,
|
||||||
|
# "is_wiki_activated": true,
|
||||||
|
# "is_issues_activated": true,
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user)
|
||||||
|
membership = f.MembershipFactory.create(user=user, project=project, is_admin=False)
|
||||||
|
membership.role.permissions = ["view_us", "view_wiki_pages"]
|
||||||
|
membership.role.save()
|
||||||
|
url = reverse("user-project-settings-list")
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 1 == len(response.data)
|
||||||
|
assert 1 == response.data[0].get("homepage")
|
||||||
|
assert 3 == len(response.data[0].get("allowed_sections"))
|
||||||
|
|
||||||
|
assert Section.timeline in response.data[0].get("allowed_sections")
|
||||||
|
assert Section.backlog in response.data[0].get("allowed_sections")
|
||||||
|
assert Section.wiki in response.data[0].get("allowed_sections")
|
||||||
|
|
||||||
|
assert Section.epics not in response.data[0].get("allowed_sections")
|
||||||
|
assert Section.issues not in response.data[0].get("allowed_sections")
|
||||||
|
|
||||||
|
|
||||||
|
def test_avoid_patch_homepage_setting_with_not_allowed_section(client):
|
||||||
|
# Default template has next configuration:
|
||||||
|
# "is_epics_activated": false,
|
||||||
|
# "is_backlog_activated": true,
|
||||||
|
# "is_kanban_activated": false,
|
||||||
|
# "is_wiki_activated": true,
|
||||||
|
# "is_issues_activated": true,
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user)
|
||||||
|
membership = f.MembershipFactory.create(user=user, project=project,
|
||||||
|
is_admin=False)
|
||||||
|
membership.role.permissions = ["view_us", "view_wiki_pages"]
|
||||||
|
membership.role.save()
|
||||||
|
|
||||||
|
setting = services.create_user_project_settings_if_not_exists(project,
|
||||||
|
project.owner)
|
||||||
|
|
||||||
|
url = reverse("user-project-settings-detail", args=[setting.pk])
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
response = client.json.patch(url, data=json.dumps({"homepage": Section.backlog}))
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response = client.json.patch(url, data=json.dumps({"homepage": Section.issues}))
|
||||||
|
assert response.status_code == 400
|
|
@ -39,6 +39,62 @@ import pytest
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def create_tasks_fixtures():
|
||||||
|
data = {}
|
||||||
|
data["project"] = f.ProjectFactory.create()
|
||||||
|
project = data["project"]
|
||||||
|
data["users"] = [f.UserFactory.create(is_superuser=True) for i in range(0, 3)]
|
||||||
|
data["roles"] = [f.RoleFactory.create() for i in range(0, 3)]
|
||||||
|
user_roles = zip(data["users"], data["roles"])
|
||||||
|
# Add membership fixtures
|
||||||
|
[f.MembershipFactory.create(user=user, project=project, role=role) for (user, role) in user_roles]
|
||||||
|
|
||||||
|
data["statuses"] = [f.TaskStatusFactory.create(project=project) for i in range(0, 4)]
|
||||||
|
data["tags"] = ["test1test2test3", "test1", "test2", "test3"]
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# | Task | Owner | Assigned To | Tags | Status |
|
||||||
|
# |-------#--------#-------------#---------------------|---------|
|
||||||
|
# | 0 | user2 | None | tag1 | status3 |
|
||||||
|
# | 1 | user1 | None | tag2 | status3 |
|
||||||
|
# | 2 | user3 | None | tag1 tag2 | status1 |
|
||||||
|
# | 3 | user2 | None | tag3 | status0 |
|
||||||
|
# | 4 | user1 | user1 | tag1 tag2 tag3 | status0 |
|
||||||
|
# | 5 | user3 | user1 | tag3 | status2 |
|
||||||
|
# | 6 | user2 | user1 | tag1 tag2 | status3 |
|
||||||
|
# | 7 | user1 | user2 | tag3 | status0 |
|
||||||
|
# | 8 | user3 | user2 | tag1 | status3 |
|
||||||
|
# | 9 | user2 | user3 | tag0 | status1 |
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
|
||||||
|
(user1, user2, user3, ) = data["users"]
|
||||||
|
(status0, status1, status2, status3 ) = data["statuses"]
|
||||||
|
(tag0, tag1, tag2, tag3, ) = data["tags"]
|
||||||
|
|
||||||
|
f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
|
status=status3, tags=[tag1])
|
||||||
|
f.TaskFactory.create(project=project, owner=user1, assigned_to=None,
|
||||||
|
status=status3, tags=[tag2])
|
||||||
|
f.TaskFactory.create(project=project, owner=user3, assigned_to=None,
|
||||||
|
status=status1, tags=[tag1, tag2])
|
||||||
|
f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
|
status=status0, tags=[tag3])
|
||||||
|
f.TaskFactory.create(project=project, owner=user1, assigned_to=user1,
|
||||||
|
status=status0, tags=[tag1, tag2, tag3])
|
||||||
|
f.TaskFactory.create(project=project, owner=user3, assigned_to=user1,
|
||||||
|
status=status2, tags=[tag3])
|
||||||
|
f.TaskFactory.create(project=project, owner=user2, assigned_to=user1,
|
||||||
|
status=status3, tags=[tag1, tag2])
|
||||||
|
f.TaskFactory.create(project=project, owner=user1, assigned_to=user2,
|
||||||
|
status=status0, tags=[tag3])
|
||||||
|
f.TaskFactory.create(project=project, owner=user3, assigned_to=user2,
|
||||||
|
status=status3, tags=[tag1])
|
||||||
|
f.TaskFactory.create(project=project, owner=user2, assigned_to=user3,
|
||||||
|
status=status1, tags=[tag0])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def test_get_tasks_from_bulk():
|
def test_get_tasks_from_bulk():
|
||||||
data = """
|
data = """
|
||||||
Task #1
|
Task #1
|
||||||
|
@ -544,6 +600,66 @@ def test_api_update_order_in_bulk_invalid_user_story_2(client):
|
||||||
assert "bulk_tasks" in response.data
|
assert "bulk_tasks" in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_update_milestone_in_bulk(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, default_task_status=None)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_admin=True)
|
||||||
|
|
||||||
|
milestone1 = f.MilestoneFactory(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory(project=project)
|
||||||
|
task1 = f.create_task(project=project, milestone=milestone1)
|
||||||
|
task2 = f.create_task(project=project, milestone=milestone1)
|
||||||
|
task3 = f.create_task(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
url = reverse("tasks-bulk-update-milestone")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_tasks": [{"task_id": task1.id, "order": 1},
|
||||||
|
{"task_id": task2.id, "order": 2},
|
||||||
|
{"task_id": task3.id, "order": 3}]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 200, response.data
|
||||||
|
assert response.data[task1.id] == milestone2.id
|
||||||
|
assert response.data[task2.id] == milestone2.id
|
||||||
|
assert response.data[task3.id] == milestone2.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_update_milestone_in_bulk_invalid_milestone(client):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
project = f.ProjectFactory.create(owner=user, default_task_status=None)
|
||||||
|
f.MembershipFactory.create(project=project, user=user, is_admin=True)
|
||||||
|
|
||||||
|
milestone1 = f.MilestoneFactory(project=project)
|
||||||
|
milestone2 = f.MilestoneFactory()
|
||||||
|
task1 = f.create_task(project=project, milestone=milestone1)
|
||||||
|
task2 = f.create_task(project=project, milestone=milestone1)
|
||||||
|
task3 = f.create_task(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
url = reverse("tasks-bulk-update-milestone")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"project_id": project.id,
|
||||||
|
"milestone_id": milestone2.id,
|
||||||
|
"bulk_tasks": [{"task_id": task1.id, "order": 1},
|
||||||
|
{"task_id": task2.id, "order": 2},
|
||||||
|
{"task_id": task3.id, "order": 3}]
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(project.owner)
|
||||||
|
|
||||||
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "milestone_id" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_get_invalid_csv(client):
|
def test_get_invalid_csv(client):
|
||||||
url = reverse("tasks-csv")
|
url = reverse("tasks-csv")
|
||||||
|
|
||||||
|
@ -736,63 +852,45 @@ def test_api_filter_by_milestone__estimated_start_and_end(client, field_name):
|
||||||
assert response.data[0]["subject"] == task.subject
|
assert response.data[0]["subject"] == task.subject
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("filter_name,collection,expected,exclude_expected,is_text", [
|
||||||
|
('status', 'statuses', 3, 7, False),
|
||||||
|
('assigned_to', 'users', 3, 7, False),
|
||||||
|
('tags', 'tags', 1, 9, True),
|
||||||
|
('owner', 'users', 3, 7, False),
|
||||||
|
('role', 'roles', 3, 7, False),
|
||||||
|
])
|
||||||
|
def test_api_filters(client, filter_name, collection, expected, exclude_expected, is_text):
|
||||||
|
data = create_tasks_fixtures()
|
||||||
|
project = data["project"]
|
||||||
|
options = data[collection]
|
||||||
|
|
||||||
|
client.login(data["users"][0])
|
||||||
|
if is_text:
|
||||||
|
param = options[0]
|
||||||
|
else:
|
||||||
|
param = options[0].id
|
||||||
|
|
||||||
|
# include test
|
||||||
|
url = "{}?project={}&{}={}".format(reverse('tasks-list'), project.id, filter_name, param)
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == expected
|
||||||
|
|
||||||
|
# exclude test
|
||||||
|
url = "{}?project={}&exclude_{}={}".format(reverse('tasks-list'), project.id, filter_name, param)
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == exclude_expected
|
||||||
|
|
||||||
|
|
||||||
def test_api_filters_data(client):
|
def test_api_filters_data(client):
|
||||||
project = f.ProjectFactory.create()
|
data = create_tasks_fixtures()
|
||||||
user1 = f.UserFactory.create(is_superuser=True)
|
project = data["project"]
|
||||||
f.MembershipFactory.create(user=user1, project=project)
|
(user1, user2, user3, ) = data["users"]
|
||||||
user2 = f.UserFactory.create(is_superuser=True)
|
(status0, status1, status2, status3, ) = data["statuses"]
|
||||||
f.MembershipFactory.create(user=user2, project=project)
|
(tag0, tag1, tag2, tag3, ) = data["tags"]
|
||||||
user3 = f.UserFactory.create(is_superuser=True)
|
|
||||||
f.MembershipFactory.create(user=user3, project=project)
|
|
||||||
|
|
||||||
status0 = f.TaskStatusFactory.create(project=project)
|
|
||||||
status1 = f.TaskStatusFactory.create(project=project)
|
|
||||||
status2 = f.TaskStatusFactory.create(project=project)
|
|
||||||
status3 = f.TaskStatusFactory.create(project=project)
|
|
||||||
|
|
||||||
tag0 = "test1test2test3"
|
|
||||||
tag1 = "test1"
|
|
||||||
tag2 = "test2"
|
|
||||||
tag3 = "test3"
|
|
||||||
|
|
||||||
# ------------------------------------------------------
|
|
||||||
# | Task | Owner | Assigned To | Tags |
|
|
||||||
# |-------#--------#-------------#---------------------|
|
|
||||||
# | 0 | user2 | None | tag1 |
|
|
||||||
# | 1 | user1 | None | tag2 |
|
|
||||||
# | 2 | user3 | None | tag1 tag2 |
|
|
||||||
# | 3 | user2 | None | tag3 |
|
|
||||||
# | 4 | user1 | user1 | tag1 tag2 tag3 |
|
|
||||||
# | 5 | user3 | user1 | tag3 |
|
|
||||||
# | 6 | user2 | user1 | tag1 tag2 |
|
|
||||||
# | 7 | user1 | user2 | tag3 |
|
|
||||||
# | 8 | user3 | user2 | tag1 |
|
|
||||||
# | 9 | user2 | user3 | tag0 |
|
|
||||||
# ------------------------------------------------------
|
|
||||||
|
|
||||||
task0 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
|
|
||||||
status=status3, tags=[tag1])
|
|
||||||
task1 = f.TaskFactory.create(project=project, owner=user1, assigned_to=None,
|
|
||||||
status=status3, tags=[tag2])
|
|
||||||
task2 = f.TaskFactory.create(project=project, owner=user3, assigned_to=None,
|
|
||||||
status=status1, tags=[tag1, tag2])
|
|
||||||
task3 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
|
|
||||||
status=status0, tags=[tag3])
|
|
||||||
task4 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user1,
|
|
||||||
status=status0, tags=[tag1, tag2, tag3])
|
|
||||||
task5 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user1,
|
|
||||||
status=status2, tags=[tag3])
|
|
||||||
task6 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user1,
|
|
||||||
status=status3, tags=[tag1, tag2])
|
|
||||||
task7 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user2,
|
|
||||||
status=status0, tags=[tag3])
|
|
||||||
task8 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user2,
|
|
||||||
status=status3, tags=[tag1])
|
|
||||||
task9 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user3,
|
|
||||||
status=status1, tags=[tag0])
|
|
||||||
|
|
||||||
url = reverse("tasks-filters-data") + "?project={}".format(project.id)
|
url = reverse("tasks-filters-data") + "?project={}".format(project.id)
|
||||||
|
|
||||||
client.login(user1)
|
client.login(user1)
|
||||||
|
|
||||||
## No filter
|
## No filter
|
||||||
|
|
|
@ -38,6 +38,71 @@ import pytest
|
||||||
pytestmark = pytest.mark.django_db(transaction=True)
|
pytestmark = pytest.mark.django_db(transaction=True)
|
||||||
|
|
||||||
|
|
||||||
|
def create_uss_fixtures():
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
data["project"] = f.ProjectFactory.create()
|
||||||
|
project = data["project"]
|
||||||
|
data["users"] = [f.UserFactory.create(is_superuser=True) for i in range(0, 3)]
|
||||||
|
data["roles"] = [f.RoleFactory.create() for i in range(0, 3)]
|
||||||
|
user_roles = zip(data["users"], data["roles"])
|
||||||
|
# Add membership fixtures
|
||||||
|
[f.MembershipFactory.create(user=user, project=project, role=role) for (user, role) in user_roles]
|
||||||
|
|
||||||
|
data["statuses"] = [f.UserStoryStatusFactory.create(project=project) for i in range(0, 4)]
|
||||||
|
data["epics"] = [f.EpicFactory.create(project=project) for i in range(0, 3)]
|
||||||
|
data["tags"] = ["test1test2test3", "test1", "test2", "test3"]
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------------------------------
|
||||||
|
# | US | Status | Owner | Assigned To | Assigned Users | Tags | Epic |
|
||||||
|
# |-------#---------#--------#-------------#---------------------#---------------------#--------------
|
||||||
|
# | 0 | status3 | user2 | None | None | tag1 | epic0 |
|
||||||
|
# | 1 | status3 | user1 | None | user1 | tag2 | None |
|
||||||
|
# | 2 | status1 | user3 | None | None | tag1 tag2 | epic1 |
|
||||||
|
# | 3 | status0 | user2 | None | None | tag3 | None |
|
||||||
|
# | 4 | status0 | user1 | user1 | None | tag1 tag2 tag3 | epic0 |
|
||||||
|
# | 5 | status2 | user3 | user1 | None | tag3 | None |
|
||||||
|
# | 6 | status3 | user2 | user1 | None | tag1 tag2 | epic0 epic2 |
|
||||||
|
# | 7 | status0 | user1 | user2 | None | tag3 | None |
|
||||||
|
# | 8 | status3 | user3 | user2 | None | tag1 | epic2 |
|
||||||
|
# | 9 | status1 | user2 | user3 | user1 | tag0 | None |
|
||||||
|
# ----------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(user1, user2, user3, ) = data["users"]
|
||||||
|
(status0, status1, status2, status3 ) = data["statuses"]
|
||||||
|
(epic0, epic1, epic2) = data["epics"]
|
||||||
|
(tag0, tag1, tag2, tag3, ) = data["tags"]
|
||||||
|
|
||||||
|
us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
|
status=status3, tags=[tag1])
|
||||||
|
f.RelatedUserStory.create(user_story=us0, epic=epic0)
|
||||||
|
us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
|
||||||
|
status=status3, tags=[tag2], assigned_users=[user1])
|
||||||
|
us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
|
||||||
|
status=status1, tags=[tag1, tag2])
|
||||||
|
f.RelatedUserStory.create(user_story=us2, epic=epic1)
|
||||||
|
us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
|
status=status0, tags=[tag3])
|
||||||
|
us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
|
||||||
|
status=status0, tags=[tag1, tag2, tag3])
|
||||||
|
f.RelatedUserStory.create(user_story=us4, epic=epic0)
|
||||||
|
us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
|
||||||
|
status=status2, tags=[tag3])
|
||||||
|
us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
|
||||||
|
status=status3, tags=[tag1, tag2])
|
||||||
|
f.RelatedUserStory.create(user_story=us6, epic=epic0)
|
||||||
|
f.RelatedUserStory.create(user_story=us6, epic=epic2)
|
||||||
|
us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
|
||||||
|
status=status0, tags=[tag3])
|
||||||
|
us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
|
||||||
|
status=status3, tags=[tag1])
|
||||||
|
f.RelatedUserStory.create(user_story=us8, epic=epic2)
|
||||||
|
us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
|
||||||
|
status=status1, tags=[tag0], assigned_users=[user1])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def test_get_userstories_from_bulk():
|
def test_get_userstories_from_bulk():
|
||||||
data = "User Story #1\nUser Story #2\n"
|
data = "User Story #1\nUser Story #2\n"
|
||||||
userstories = services.get_userstories_from_bulk(data)
|
userstories = services.get_userstories_from_bulk(data)
|
||||||
|
@ -777,72 +842,14 @@ def test_api_filter_by_milestone__estimated_start_and_end(client, field_name):
|
||||||
|
|
||||||
|
|
||||||
def test_api_filters_data(client):
|
def test_api_filters_data(client):
|
||||||
project = f.ProjectFactory.create()
|
data = create_uss_fixtures()
|
||||||
user1 = f.UserFactory.create(is_superuser=True)
|
project = data["project"]
|
||||||
f.MembershipFactory.create(user=user1, project=project)
|
(user1, user2, user3, ) = data["users"]
|
||||||
user2 = f.UserFactory.create(is_superuser=True)
|
(status0, status1, status2, status3, ) = data["statuses"]
|
||||||
f.MembershipFactory.create(user=user2, project=project)
|
(epic0, epic1, epic2, ) = data["epics"]
|
||||||
user3 = f.UserFactory.create(is_superuser=True)
|
(tag0, tag1, tag2, tag3, ) = data["tags"]
|
||||||
f.MembershipFactory.create(user=user3, project=project)
|
|
||||||
|
|
||||||
status0 = f.UserStoryStatusFactory.create(project=project)
|
|
||||||
status1 = f.UserStoryStatusFactory.create(project=project)
|
|
||||||
status2 = f.UserStoryStatusFactory.create(project=project)
|
|
||||||
status3 = f.UserStoryStatusFactory.create(project=project)
|
|
||||||
|
|
||||||
epic0 = f.EpicFactory.create(project=project)
|
|
||||||
epic1 = f.EpicFactory.create(project=project)
|
|
||||||
epic2 = f.EpicFactory.create(project=project)
|
|
||||||
|
|
||||||
tag0 = "test1test2test3"
|
|
||||||
tag1 = "test1"
|
|
||||||
tag2 = "test2"
|
|
||||||
tag3 = "test3"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------------------------------
|
|
||||||
# | US | Status | Owner | Assigned To | Assigned Users | Tags | Epic |
|
|
||||||
# |-------#---------#--------#-------------#---------------------#---------------------#--------------
|
|
||||||
# | 0 | status3 | user2 | None | None | tag1 | epic0 |
|
|
||||||
# | 1 | status3 | user1 | None | user1 | tag2 | None |
|
|
||||||
# | 2 | status1 | user3 | None | None | tag1 tag2 | epic1 |
|
|
||||||
# | 3 | status0 | user2 | None | None | tag3 | None |
|
|
||||||
# | 4 | status0 | user1 | user1 | None | tag1 tag2 tag3 | epic0 |
|
|
||||||
# | 5 | status2 | user3 | user1 | None | tag3 | None |
|
|
||||||
# | 6 | status3 | user2 | user1 | None | tag1 tag2 | epic0 epic2 |
|
|
||||||
# | 7 | status0 | user1 | user2 | None | tag3 | None |
|
|
||||||
# | 8 | status3 | user3 | user2 | None | tag1 | epic2 |
|
|
||||||
# | 9 | status1 | user2 | user3 | user1 | tag0 | None |
|
|
||||||
# ----------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
|
||||||
status=status3, tags=[tag1])
|
|
||||||
f.RelatedUserStory.create(user_story=us0, epic=epic0)
|
|
||||||
us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
|
|
||||||
status=status3, tags=[tag2], assigned_users=[user1])
|
|
||||||
us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
|
|
||||||
status=status1, tags=[tag1, tag2])
|
|
||||||
f.RelatedUserStory.create(user_story=us2, epic=epic1)
|
|
||||||
us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
|
||||||
status=status0, tags=[tag3])
|
|
||||||
us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
|
|
||||||
status=status0, tags=[tag1, tag2, tag3])
|
|
||||||
f.RelatedUserStory.create(user_story=us4, epic=epic0)
|
|
||||||
us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
|
|
||||||
status=status2, tags=[tag3])
|
|
||||||
us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
|
|
||||||
status=status3, tags=[tag1, tag2])
|
|
||||||
f.RelatedUserStory.create(user_story=us6, epic=epic0)
|
|
||||||
f.RelatedUserStory.create(user_story=us6, epic=epic2)
|
|
||||||
us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
|
|
||||||
status=status0, tags=[tag3])
|
|
||||||
us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
|
|
||||||
status=status3, tags=[tag1])
|
|
||||||
f.RelatedUserStory.create(user_story=us8, epic=epic2)
|
|
||||||
us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
|
|
||||||
status=status1, tags=[tag0], assigned_users=[user1])
|
|
||||||
|
|
||||||
url = reverse("userstories-filters-data") + "?project={}".format(project.id)
|
url = reverse("userstories-filters-data") + "?project={}".format(project.id)
|
||||||
|
|
||||||
client.login(user1)
|
client.login(user1)
|
||||||
|
|
||||||
# No filter
|
# No filter
|
||||||
|
@ -961,6 +968,37 @@ def test_api_filters_data(client):
|
||||||
assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2
|
assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("filter_name,collection,expected,exclude_expected,is_text", [
|
||||||
|
('status', 'statuses', 3, 7, False),
|
||||||
|
('tags', 'tags', 1, 9, True),
|
||||||
|
('owner', 'users', 3, 7, False),
|
||||||
|
('role', 'roles', 5, 5, False),
|
||||||
|
('assigned_users', 'users', 5, 5, False),
|
||||||
|
])
|
||||||
|
def test_api_filters(client, filter_name, collection, expected, exclude_expected, is_text):
|
||||||
|
data = create_uss_fixtures()
|
||||||
|
project = data["project"]
|
||||||
|
options = data[collection]
|
||||||
|
|
||||||
|
client.login(data["users"][0])
|
||||||
|
if is_text:
|
||||||
|
param = options[0]
|
||||||
|
else:
|
||||||
|
param = options[0].id
|
||||||
|
|
||||||
|
# include test
|
||||||
|
url = "{}?project={}&{}={}".format(reverse('userstories-list'), project.id, filter_name, param)
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == expected
|
||||||
|
|
||||||
|
# exclude test
|
||||||
|
url = "{}?project={}&exclude_{}={}".format(reverse('userstories-list'), project.id, filter_name, param)
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert len(response.data) == exclude_expected
|
||||||
|
|
||||||
|
|
||||||
def test_api_filters_data_with_assigned_users(client):
|
def test_api_filters_data_with_assigned_users(client):
|
||||||
project = f.ProjectFactory.create()
|
project = f.ProjectFactory.create()
|
||||||
user1 = f.UserFactory.create(is_superuser=True)
|
user1 = f.UserFactory.create(is_superuser=True)
|
||||||
|
|
|
@ -85,7 +85,7 @@ def test_proccessor_valid_us_reference():
|
||||||
instance.content_type.model = "userstory"
|
instance.content_type.model = "userstory"
|
||||||
instance.content_object.subject = "test"
|
instance.content_object.subject = "test"
|
||||||
result = render(dummy_project, "**#1**")
|
result = render(dummy_project, "**#1**")
|
||||||
expected_result = '<p><strong><a class="reference user-story" href="http://localhost:9001/project/test/us/1" title="#1 test">#1</a></strong></p>'
|
expected_result = '<p><strong><a class="reference user-story" href="http://localhost:9001/project/test/us/1" title="#1 test">#1</a></strong></p>'
|
||||||
assert result == expected_result
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ def test_proccessor_valid_issue_reference():
|
||||||
instance.content_type.model = "issue"
|
instance.content_type.model = "issue"
|
||||||
instance.content_object.subject = "test"
|
instance.content_object.subject = "test"
|
||||||
result = render(dummy_project, "**#2**")
|
result = render(dummy_project, "**#2**")
|
||||||
expected_result = '<p><strong><a class="reference issue" href="http://localhost:9001/project/test/issue/2" title="#2 test">#2</a></strong></p>'
|
expected_result = '<p><strong><a class="reference issue" href="http://localhost:9001/project/test/issue/2" title="#2 test">#2</a></strong></p>'
|
||||||
assert result == expected_result
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ def test_proccessor_valid_task_reference():
|
||||||
instance.content_type.model = "task"
|
instance.content_type.model = "task"
|
||||||
instance.content_object.subject = "test"
|
instance.content_object.subject = "test"
|
||||||
result = render(dummy_project, "**#3**")
|
result = render(dummy_project, "**#3**")
|
||||||
expected_result = '<p><strong><a class="reference task" href="http://localhost:9001/project/test/task/3" title="#3 test">#3</a></strong></p>'
|
expected_result = '<p><strong><a class="reference task" href="http://localhost:9001/project/test/task/3" title="#3 test">#3</a></strong></p>'
|
||||||
assert result == expected_result
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@ -207,13 +207,13 @@ def test_render_relative_image():
|
||||||
|
|
||||||
|
|
||||||
def test_render_triple_quote_code():
|
def test_render_triple_quote_code():
|
||||||
expected_result = '<div class="codehilite"><pre><span></span><span class="k">print</span><span class="p">(</span><span class="s2">"test"</span><span class="p">)</span>\n</pre></div>'
|
expected_result = '<div class="codehilite"><pre><span></span><span class="k">print</span><span class="p">(</span><span class="s2">"test"</span><span class="p">)</span>\n</pre></div>'
|
||||||
|
|
||||||
assert render(dummy_project, "```python\nprint(\"test\")\n```") == expected_result
|
assert render(dummy_project, "```python\nprint(\"test\")\n```") == expected_result
|
||||||
|
|
||||||
|
|
||||||
def test_render_triple_quote_and_lang_code():
|
def test_render_triple_quote_and_lang_code():
|
||||||
expected_result = '<div class="codehilite"><pre><span></span><span class="k">print</span><span class="p">(</span><span class="s2">"test"</span><span class="p">)</span>\n</pre></div>'
|
expected_result = '<div class="codehilite"><pre><span></span><span class="k">print</span><span class="p">(</span><span class="s2">"test"</span><span class="p">)</span>\n</pre></div>'
|
||||||
|
|
||||||
assert render(dummy_project, "```python\nprint(\"test\")\n```") == expected_result
|
assert render(dummy_project, "```python\nprint(\"test\")\n```") == expected_result
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .. import factories as f
|
||||||
|
from taiga.projects.milestones import services
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_issues_not_closed():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_issues_closed():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
|
||||||
|
assert services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_issues_but_closed_tasks():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
tasks_closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1,
|
||||||
|
taskboard_order=1, status=tasks_closed_status)
|
||||||
|
f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_issues_but_closed_uss():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
us_closed_status = f.UserStoryStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
status=us_closed_status, is_closed=True)
|
||||||
|
|
||||||
|
f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_closed_issues_but_open_uss():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
|
||||||
|
f.create_userstory(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_closed_issues_but_open_tasks():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
|
||||||
|
f.create_task(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_not_closed():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
f.create_task(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_closed():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1,
|
||||||
|
status=closed_status, user_story=None)
|
||||||
|
|
||||||
|
assert services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_tasks_but_closed_issues():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
issue_closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=issue_closed_status)
|
||||||
|
f.create_task(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_tasks_but_closed_uss():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
us_closed_status = f.UserStoryStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
status=us_closed_status, is_closed=True)
|
||||||
|
|
||||||
|
f.create_task(project=project, milestone=milestone1, user_story=None)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_closed_tasks_but_open_uss():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1,
|
||||||
|
status=closed_status, user_story=None)
|
||||||
|
|
||||||
|
f.create_userstory(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_closed_tasks_but_open_issues():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1,
|
||||||
|
status=closed_status, user_story=None)
|
||||||
|
|
||||||
|
f.create_issue(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_uss_not_closed():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.UserStoryStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
status=closed_status, is_closed=True)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_uss_closed():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.UserStoryStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
sprint_order=1, status=closed_status,
|
||||||
|
is_closed=True)
|
||||||
|
|
||||||
|
assert services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_uss_but_closed_tasks_and_us():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
us_closed_status = f.UserStoryStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
us = f.create_userstory(project=project, milestone=milestone1,
|
||||||
|
status=us_closed_status, is_closed=True)
|
||||||
|
|
||||||
|
task_closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1, user_story=us,
|
||||||
|
status=task_closed_status)
|
||||||
|
|
||||||
|
f.create_userstory(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_uss_but_closed_tasks():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.TaskStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_task(project=project, milestone=milestone1,
|
||||||
|
status=closed_status, user_story=None)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stay_open_with_uss_but_closed_issues():
|
||||||
|
project = f.ProjectFactory()
|
||||||
|
f.MembershipFactory.create(project=project, user=project.owner,
|
||||||
|
is_admin=True)
|
||||||
|
milestone1 = f.MilestoneFactory.create(project=project)
|
||||||
|
|
||||||
|
closed_status = f.IssueStatusFactory.create(project=project,
|
||||||
|
is_closed=True)
|
||||||
|
f.create_issue(project=project, milestone=milestone1,
|
||||||
|
status=closed_status)
|
||||||
|
f.create_userstory(project=project, milestone=milestone1)
|
||||||
|
|
||||||
|
assert not services.calculate_milestone_is_closed(milestone1)
|
Loading…
Reference in New Issue