diff --git a/.circleci/config.yml b/.circleci/config.yml index a86a1a30e4..6b83af589e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ jobs: - run: name: Run Tests command: | - tox -e d21,report + tox -e d22,report - save_cache: key: deps2-{{ .Branch }}--{{ checksum "Pipfile.lock" }}-{{ checksum ".circleci/config.yml" }} paths: diff --git a/Pipfile b/Pipfile index 30ad470709..ffe229be82 100644 --- a/Pipfile +++ b/Pipfile @@ -23,38 +23,37 @@ carto = "==1.4" celery = "==4.3" dj-database-url = "==0.5" dj-static = "==0.0.6" -Django = "==2.1.8" +Django = "==2.2.1" django-appconf = "==1.0.3" django_celery_beat = "==1.4" django-celery-email = "==2.0.1" django_celery_results = "==1.0.4" django-contrib-comments = "==1.9.1" -django-cors-headers = "==2.5" +django-cors-headers = "==2.5.3" django-debug-toolbar = "==1.11" django-extensions = "==2.1.6" django-easy-pdf = "==0.1.1" django-filter = "==2.1" -django-fsm = "==2.6" +django-fsm = "==2.6.1" django-import-export = "==1.2" django-js-asset = "==1.2.2" django-leaflet = "==0.24" django-logentry-admin = "==1.0.4" django-model-utils = "==3.1.2" -django-mptt = "==0.9.1" django-ordered-model = "==3.1.1" django-post_office = "==3.1.0" django-redis-cache = "==2.0" django-rest-swagger = "==2.2" django-storages = "==1.6.6" -django-tenants = "==2.1" +django-tenants = "==2.2.2" django-timezone-field = "==3.0" -django-waffle = "==0.15.1" +django-waffle = "==0.16" djangorestframework-csv = "==2.1.0" djangorestframework-gis = "==0.14" djangorestframework-jwt = "==1.11.0" djangorestframework-recursive = "==0.1.2" djangorestframework-xml = "==1.4" -djangorestframework = "==3.9.2" +djangorestframework = "==3.9.3" drf-nested-routers = "==0.91" drf-querystringfilter = "==1.0.0" etools-validator = "==0.3.2" @@ -64,20 +63,21 @@ gunicorn = "==19.9" newrelic = "==2.94.0.79" Pillow = "==5.4.1" psycopg2-binary = "==2.8.2" -sentry-sdk = "==0.7.10" -redis = "==3.2" +sentry-sdk = "==0.7.14" +redis = "==3.2.1" requests = "==2.21" -social-auth-app-django = "==2.1.0" -social-auth-core = {extras = ["azuread"],version = "==1.7.0"} +social-auth-app-django = "==3.1" +social-auth-core = {extras = ["azuread"],version = "==3.1"} tenant-schemas-celery = "==0.2.1" unicef_attachments = "==0.5.1" unicef-djangolib = "==0.5.2" -unicef-locations = "==1.5" +unicef-locations = "==1.7" unicef_notification = "==0.2.1" unicef_restlib = "==0.4" unicef_snapshot = "==0.2.3" unicef-rest-export = "==0.5.3" xhtml2pdf = "==0.2.3" +unicef-vision = "*" [requires] python_version = "3.6.4" diff --git a/Pipfile.lock b/Pipfile.lock index 1756d1f8ec..2eb81abdd1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ebd440f956d33b04dc1801186a9be40fd9a9cf9158152a12858cab0c3d50d778" + "sha256": "04bd293baba543ab45eecb5b5c140ee4f63d68b2a4a2dfbc155a451ccf467082" }, "pipfile-spec": 6, "requires": { @@ -32,10 +32,10 @@ }, "azure-common": { "hashes": [ - "sha256:14722caf6c3ed81d2cfdd3e448635fdc78f214dc6f17558dd1ca5b87bccc0631", - "sha256:622d9360a1b61172b4c0d1cc58f939c68402aa19ca44872ab3d224d913aa6d0c" + "sha256:76e5b85074e25613ad698db2b02f88ed2c43b2fe56d11e03588a6f6e6f5b4d5a", + "sha256:80091201f404850b8b2b2c2bf38df1348216b6ed90690f529335879c9ef080f3" ], - "version": "==1.1.19" + "version": "==1.1.20" }, "azure-nspkg": { "hashes": [ @@ -205,11 +205,11 @@ }, "django": { "hashes": [ - "sha256:0fd54e4f27bc3e0b7054a11e6b3a18fa53f2373f6b2df8a22e8eadfe018970a5", - "sha256:f3b28084101d516f56104856761bc247f85a2a5bbd9da39d9f6197ff461b3ee4" + "sha256:6fcc3cbd55b16f9a01f37de8bcbe286e0ea22e87096557f1511051780338eaea", + "sha256:bb407d0bb46395ca1241f829f5bd03f7e482f97f7d1936e26e98dacb201ed4ec" ], "index": "pypi", - "version": "==2.1.8" + "version": "==2.2.1" }, "django-appconf": { "hashes": [ @@ -219,6 +219,12 @@ "index": "pypi", "version": "==1.0.3" }, + "django-autocomplete-light": { + "hashes": [ + "sha256:cff0b1cad0e233e49c8cce08dff22868951123cbb79a7c1768eda78845044568" + ], + "version": "==3.3.4" + }, "django-celery-beat": { "hashes": [ "sha256:3c2c22647455be5503aca7450db64ea53acacee2d0aef3d7ac49aa3ef3845724", @@ -253,11 +259,11 @@ }, "django-cors-headers": { "hashes": [ - "sha256:4f39b4af6b3a9aaf54e6711a60ecee1d2c4ed3056395ab6626d7ed17555c8fce", - "sha256:a8aeae8b56d9a7a1f57e9096e9e0dc6cfead2ecea4d5c4d51c1fd66024ac390a" + "sha256:c7987faa9aaef7f6a802b0f354a719e80a9158c284f530265ac792f1ee725e52", + "sha256:ceacbd60dd5a65c95e65e74b5559bd4161aa3fe5713c44e1f3417a12bd41e7ba" ], "index": "pypi", - "version": "==2.5" + "version": "==2.5.3" }, "django-debug-toolbar": { "hashes": [ @@ -292,11 +298,11 @@ }, "django-fsm": { "hashes": [ - "sha256:13da2fbec4284dcf2235157c299306f67b788bd39126bed3cdbcc6a8f17575e9", - "sha256:c4790469e4f54b6cc95a3ac2c1bd1cdadeb04b0f3578e2472cdd857543a56e67" + "sha256:96f776736042b8cde03483cb4b27c3a62580da40fc9b942845349718afdf5d0f", + "sha256:cacd5485c726b411c6728c9ae3120303c9badf8aa0edff2db3aecdbe3a906945" ], "index": "pypi", - "version": "==2.6" + "version": "==2.6.1" }, "django-import-export": { "hashes": [ @@ -338,11 +344,10 @@ }, "django-mptt": { "hashes": [ - "sha256:18a41d1b56ca7c02a5b04d246e33ee2d18f6ee5459c02ed1d945f5abdef23a2e", - "sha256:689a04cce0981671d6061a9928c33a16b47abb0d4cd43cf7dec31ae284fdae9d" + "sha256:6bf9eb26e54e92006ca82108a1c946c7df533ec27bddf3f795a83a32a3d1b04b", + "sha256:c765c1501dd0b5c22f0ca8b948550bd294cd2db68aefa0560c6ed7fcfdf4b95e" ], - "index": "pypi", - "version": "==0.9.1" + "version": "==0.10.0" }, "django-ordered-model": { "hashes": [ @@ -384,10 +389,10 @@ }, "django-tenants": { "hashes": [ - "sha256:43abc01d47f9b9ab62b8ff5ba09d995652b53c10d71fdd146afaec51f55d05af" + "sha256:5c6ca5d291875073f4d34c5d84786f4a7c3ef973e8ebbc4334c0e2393a31db77" ], "index": "pypi", - "version": "==2.1" + "version": "==2.2.2" }, "django-timezone-field": { "hashes": [ @@ -399,19 +404,19 @@ }, "django-waffle": { "hashes": [ - "sha256:55691686e3cd6bc0116cf9f025cc7b49b493e8dac8e43f5b4c303d5b8d865588", - "sha256:5ee5b0a3dc6b5e19dcc60f1c02422343b3017b6559a2f1b68705e0d07617aed9" + "sha256:806164c8e98fe6c20590bd642fa247d8554ab57faed45e2745253f0f3b8f3ff9", + "sha256:b48b80b01ea28a560a0d6df9322631f239f4088933137a39571d1c874cc89ce9" ], "index": "pypi", - "version": "==0.15.1" + "version": "==0.16" }, "djangorestframework": { "hashes": [ - "sha256:8a435df9007c8b7d8e69a21ef06650e3c0cbe0d4b09e55dd1bd74c89a75a9fcd", - "sha256:f7a266260d656e1cf4ca54d7a7349609dc8af4fe2590edd0ecd7d7643ea94a17" + "sha256:1d22971a5fc98becdbbad9710ca2a9148dd339f6cbea4c3ddbed2cb84bab94e1", + "sha256:2884763160b997073ff1e937bd820a69d23978902a3ccd0ba53a217e196239f0" ], "index": "pypi", - "version": "==3.9.2" + "version": "==3.9.3" }, "djangorestframework-csv": { "hashes": [ @@ -530,10 +535,10 @@ }, "jdcal": { "hashes": [ - "sha256:948fb8d079e63b4be7a69dd5f0cd618a0a57e80753de8248fd786a8a20658a07", - "sha256:ea0a5067c5f0f50ad4c7bdc80abad3d976604f6fb026b0b3a17a9d84bb9046c9" + "sha256:1abf1305fce18b4e8aa248cf8fe0c56ce2032392bc64bbd61b5dff2a19ec8bba", + "sha256:472872e096eb8df219c23f2689fc336668bdb43d194094b5cc1707e1640acfc8" ], - "version": "==1.4" + "version": "==1.4.1" }, "jinja2": { "hashes": [ @@ -688,6 +693,22 @@ "index": "pypi", "version": "==5.4.1" }, + "psycopg2": { + "hashes": [ + "sha256:00cfecb3f3db6eb76dcc763e71777da56d12b6d61db6a2c6ccbbb0bff5421f8f", + "sha256:076501fc24ae13b2609ba2303d88d4db79072562f0b8cc87ec1667dedff99dc1", + "sha256:4e2b34e4c0ddfeddf770d7df93e269700b080a4d2ec514fec668d71895f56782", + "sha256:5cacf21b6f813c239f100ef78a4132056f93a5940219ec25d2ef833cbeb05588", + "sha256:61f58e9ecb9e4dc7e30be56b562f8fc10ae3addcfcef51b588eed10a5a66100d", + "sha256:8954ff6e47247bdd134db602fcadfc21662835bd92ce0760f3842eacfeb6e0f3", + "sha256:b6e8c854cdc623028e558a409b06ea2f16d13438335941c7765d0a42b5bedd33", + "sha256:baca21c0f7344576346e260454d0007313ccca8c170684707a63946b27a56c8f", + "sha256:bb1735378770fb95dbe392d29e71405d45c8bdcfa064f916504833a92ab03c55", + "sha256:de3d3c46c1ee18f996db42d1eb44cf1565cc9e38fb1dbd9b773ff6b3fa8035d7", + "sha256:dee885602bb200bdcb1d30f6da6c7bb207360bc786d0a364fe1540dd14af0bab" + ], + "version": "==2.8.2" + }, "psycopg2-binary": { "hashes": [ "sha256:007ca0df127b1862fc010125bc4100b7a630efc6841047bd11afceadb4754611", @@ -806,44 +827,44 @@ }, "redis": { "hashes": [ - "sha256:724932360d48e5407e8f82e405ab3650a36ed02c7e460d1e6fddf0f038422b54", - "sha256:9b19425a38fd074eb5795ff2b0d9a55b46a44f91f5347995f27e3ad257a7d775" + "sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175", + "sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273" ], "index": "pypi", - "version": "==3.2.0" + "version": "==3.2.1" }, "reportlab": { "hashes": [ - "sha256:1c228a3ac2c405f7fc16eac43ba92aec448bc25438902f30590ad021e8828097", - "sha256:2210fafd3bb06308a84876fe6d19172b645373edce2b6d7501378cb9c768f825", - "sha256:232fb2037b7c3df259685f1c5ecb7826f55742dc81f0713837b84a152307483e", - "sha256:2c4f25e63fa75f3064871cf435696a4e19b7bd4901d922b766ae58a447b5b6da", - "sha256:47951166d897b60e9e7ca349db82a2b689e6478ac6078e2c7c88ca8becbb0c7d", - "sha256:526ab1193ea8e97c4838135917890e66de5f777d04283008007229b139f3c094", - "sha256:5a9cc8470623ec5b76c7e59f56b7d1fcf0254896cd61842dbdbd278934cc50f4", - "sha256:5ddc1a4a74f225e35a7f60e2eae10de6878dddc9960dad2d9cadc49092f8850d", - "sha256:6b594f6d7d71bc5778e19adb1c699a598c69b9a7bcf97fa638d8762279f9d80a", - "sha256:6e8c89b46cfaf9ae40b7db87e9f29c9e5d32d18d25f9cd10d423a5241e8ec453", - "sha256:71f4f3e3975b91ddbfc1b36a537b46d07533ca7f31945e990a75db5f9bd7a0ba", - "sha256:763654dc346eeb66fa726a88d27f911339950d20a25303dfc098f3b59ba26614", - "sha256:7bae4b33363f44343e0fac5004c8e44576c3ed00885be4eee1f2260802c116c3", - "sha256:8a4b8a0fd0547f3b436b548284aa604ba183bfac26f41a7ffb23d0ff5db8c658", - "sha256:8b08d68e4cb498eabf85411beda5c32e591ef8d0a6d18c948c3f80ed5d2c6e31", - "sha256:9840f27948b54aefa3c6386e5ed0f124d641eb54fa2f2bc9aebcb270598487fc", - "sha256:9ae8f822370e47486ba1880f7580669058a41e64bdaa41019f4617317489f884", - "sha256:9db49197080646a113059eba1c0758161164de1bc57315e7422bbf8c86e03dcf", - "sha256:a08d23fa3f23f13a1cc6dca3b3c431d08ae48e52384e6bf47bbefb22fde58e61", - "sha256:ac111bc47733dbfa3e34d61282c91b69b1f66800b0c72b7b86dc2534faa09bef", - "sha256:bc3c69707c0bf9308193612d34ca87249d6fc91a35ce0873102321395d39024a", - "sha256:c375759a763c1c93d5b4f36620390440d9fa6dec6fcf88bce8234701d88b339c", - "sha256:c8a5988d73ec93a54f22660b64c5f3d2018163dd9ca4a5cdde8022a7e4fcb345", - "sha256:eba2bc7c28a3b2b0a3c24caff33e4d8708db008f480b03a6ea39c28661663746", - "sha256:ee187977d587b9b81929e08022f385eb11274efd75795d59d99eb23b3fa9b055", - "sha256:f3ef7616ffc27c150ffec61ac820739495f6a9ca5d8532047102756ebb27e8d1", - "sha256:f46f223fcae09c8bf2746b4eb2f351294faae04b262429cc480d34c69b133fd9", - "sha256:fd9f6429a68a246fb466696d97d1240752c889b5bfdc219fea15ae787cf366a6" - ], - "version": "==3.5.19" + "sha256:04b9bf35127974f734bddddf48860732361e31c1220c0ebe4f683f19d5cfc3b8", + "sha256:073da867efdf9e0d6cba2a566f5929ef0bb9fb757b53a7132b91db9869441859", + "sha256:08e6e63a4502d3a00062ba9ff9669f95577fbdb1a5f8c6cdb1230c5ee295273a", + "sha256:0960567b9d937a288efa04753536dce1dbb032a1e1f622fd92efbe85b8cccf6e", + "sha256:1870e321c5d7772fd6e5538a89562ed8b40687ed0aec254197dc73e9d700e62f", + "sha256:1eac902958a7f66c30e1115fa1a80bf6a7aa57680427cfcb930e13c746142150", + "sha256:1f6cdcdaf6ab78ab3efd21b23c27e4487a5c0816202c3578b277f441f984a51f", + "sha256:281443252a335489ce4b8b150afccdc01c74daf97e962fd99a8c2d59c8b333d3", + "sha256:2ae66e61b03944c5ed1f3c96bbc51160cce4aa28cbe96f205b464017cdfc851c", + "sha256:34d348575686390676757876fef50f6e32e3a59ff7d549e022b5f3b8a9f7e564", + "sha256:508224a11ec9ef203ae2fd2177e903d36d3b840eeb8ac70747f53eeb373db439", + "sha256:5c497c9597a346d27007507cddc2a792f8ca5017268738fd35c374c224d81988", + "sha256:6e0d9efe78526ddf5ad1d2357f6b2b0f5d7df354ac559358e3d056bdd12fdabf", + "sha256:817dfd400c5e694cbb6eb87bc932cd3d97cf5d79d918329b8f99085a7979bb29", + "sha256:8d6ed4357eb0146501ebdb7226c87ef98a9bcbc6d54401ec676fa905b6355e00", + "sha256:8e681324ce457cc3d5c0949c92d590ac4401347b5df55f6fde207b42316d42d2", + "sha256:926981544d37554b44c6f067c3f94981831f9ef3f2665fa5f4114b23a140f596", + "sha256:92a0bf5cc2d9418115bff46032964d25bb21c0ac8bcdf6bee5769ca810a54a5a", + "sha256:9a3e7495e223fc4a9bdcd356972c230d32bf8c7a57442ca5b8c2ff6b19e6007b", + "sha256:a31f424020176e96a0ff0229f7f251d865c5409ddf074f695b97ba604f173b48", + "sha256:aa0c35b22929c19ecd48d5c1734e420812f269f463d1ef138e0adb28069c3150", + "sha256:b36b555cdbdd51f9f00a7606966ec6d4d30d74c61d1523a1ac56bbeb83a15ed3", + "sha256:cd3d9765b8f446c25d75a4456d8781c4781de0f10f860dff5cb69bbe526e8f53", + "sha256:d3daa4f19d1dc2fc1fc2591e1354edd95439b9e9953ca8b374d41524d434b315", + "sha256:d8f1878bc1fc91c63431e9b0f1940ff18b70c059f6d38f2be1e34ce9ffcc28ea", + "sha256:ddca7479d29f9dfbfc69057764239ec7753b49a3b0dcbed08f70cbef8fccfee6", + "sha256:f28f3a965d15c88c797cf33968bdaa5a04aabcf321d3f6fcf14d7e7fde8d90f3", + "sha256:fcca214bf340f59245fff792134a9ac333d21eeef19a874a69ecc926b4c992a4" + ], + "version": "==3.5.21" }, "requests": { "hashes": [ @@ -862,11 +883,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:ca2723556c102a1fabdf461b9a038d1d8631608c4d10085a7c06a0b590e79ad4", - "sha256:ced85a48171b3421d71f14f1682168f8008581411893e42359469c397fdf6285" + "sha256:5818289868755cfea74e61e532b4b0d11d523901041338d473277db91d4d8173", + "sha256:b50948bbb553eef11ba650db858e31f5bb7c8d821a9d7338a01d01487d964e8c" ], "index": "pypi", - "version": "==0.7.10" + "version": "==0.7.14" }, "simplejson": { "hashes": [ @@ -894,24 +915,24 @@ }, "social-auth-app-django": { "hashes": [ - "sha256:25295c94375d28062f65d0b3cd4b7e304c7e2fb7bac1ec51b40650a654c352f4", - "sha256:4e34d86709bfa51fd2938307e00f12f4e1dfef8a8ed6bfbfe9aeb4e69d57148f", - "sha256:b7c28bef8fbd11ff357ddd885cb219cdb55565e01109c709dd28569e0bfb0dea" + "sha256:6d0dd18c2d9e71ca545097d57b44d26f59e624a12833078e8e52f91baf849778", + "sha256:9237e3d7b6f6f59494c3b02e0cce6efc69c9d33ad9d1a064e3b2318bcbe89ae3", + "sha256:f151396e5b16e2eee12cd2e211004257826ece24fc4ae97a147df386c1cd7082" ], "index": "pypi", - "version": "==2.1.0" + "version": "==3.1" }, "social-auth-core": { "extras": [ "azuread" ], "hashes": [ - "sha256:273eb5bbeded3cfc178ca7e14f0641165df03133a2f787a6e412f782489d56ba", - "sha256:7b393754ab75f6e5176568554f4f7b5cd9e4cb6dab23d9614e5c9e1425f3fcf9", - "sha256:eb0d0e29d0cfa729cd52437314d4aeb83806c4d6e7824cbe988195b6a4b85163" + "sha256:65122fb4287c70ff7915be0f52150fc1a9b9515eab3c3f0e4cd9dbb2a442a5c3", + "sha256:cc871fb4528f7cbba67efdba0bc0f7d7c6eeb92113b0cdc9368dd91ffe965782", + "sha256:f9f36dfa6af2823efb35a5ef65dfd02f66c944f389c33c25dd9621f8bb75a7da" ], "index": "pypi", - "version": "==1.7.0" + "version": "==3.1.0" }, "sqlparse": { "hashes": [ @@ -969,10 +990,10 @@ }, "unicef-locations": { "hashes": [ - "sha256:5dde31cb0959acb8bc868f89419caa5d8e736cfc8cd45248fe2b21a107a12883" + "sha256:28103d464ac30a19f492e24ea391d9caa703c0399869d955ce30e158e7bcbd06" ], "index": "pypi", - "version": "==1.5" + "version": "==1.7" }, "unicef-notification": { "hashes": [ @@ -1002,6 +1023,14 @@ "index": "pypi", "version": "==0.2.3" }, + "unicef-vision": { + "hashes": [ + "sha256:556677d014a910004f762622008a5cdb7eb51275413b334cc06b3a8e250dde0a", + "sha256:fff832660e4f48ed31110157ae33ff1acaf18912ff484fe26e271e8441dcbc6c" + ], + "index": "pypi", + "version": "==0.1" + }, "unicodecsv": { "hashes": [ "sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc" @@ -1018,10 +1047,10 @@ }, "urllib3": { "hashes": [ - "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", - "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" ], - "version": "==1.24.2" + "version": "==1.24.3" }, "vine": { "hashes": [ @@ -1141,11 +1170,11 @@ }, "djangorestframework": { "hashes": [ - "sha256:8a435df9007c8b7d8e69a21ef06650e3c0cbe0d4b09e55dd1bd74c89a75a9fcd", - "sha256:f7a266260d656e1cf4ca54d7a7349609dc8af4fe2590edd0ecd7d7643ea94a17" + "sha256:1d22971a5fc98becdbbad9710ca2a9148dd339f6cbea4c3ddbed2cb84bab94e1", + "sha256:2884763160b997073ff1e937bd820a69d23978902a3ccd0ba53a217e196239f0" ], "index": "pypi", - "version": "==3.9.2" + "version": "==3.9.3" }, "docutils": { "hashes": [ @@ -1171,18 +1200,18 @@ }, "factory-boy": { "hashes": [ - "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca", - "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f" + "sha256:728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee", + "sha256:faf48d608a1735f0d0a3c9cbf536d64f9132b547dae7ba452c4d99a79e84a370" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.12.0" }, "faker": { "hashes": [ - "sha256:167cef2454482dc2fbd8b0ff6a5ba3dbac8d3a3ebdee6ba819d008100d9d9428", - "sha256:3f2f4570df28df2eb8f39b00520eb610081d6552975e926c6a2cbc64fd89c4c1" + "sha256:1c0a5e7bb54d2c54569986a27124715c83899e592d8d61d4e372dbff6c699573", + "sha256:60477f757a80f665bbe1fb3d1cfe5d205ec7b99d5240114de7b27b4c25d236ca" ], - "version": "==1.0.5" + "version": "==1.0.7" }, "fancycompleter": { "hashes": [ @@ -1229,11 +1258,11 @@ }, "ipython": { "hashes": [ - "sha256:b038baa489c38f6d853a3cfc4c635b0cda66f2864d136fe8f40c1a6e334e2a6b", - "sha256:f5102c1cd67e399ec8ea66bcebe6e3968ea25a8977e53f012963e5affeb1fe38" + "sha256:54c5a8aa1eadd269ac210b96923688ccf01ebb2d0f21c18c3c717909583579a8", + "sha256:e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26" ], "index": "pypi", - "version": "==7.4.0" + "version": "==7.5.0" }, "ipython-genutils": { "hashes": [ @@ -1244,11 +1273,11 @@ }, "isort": { "hashes": [ - "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43", - "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a" + "sha256:49293e2ff590cc8d48bc1f51970548b5b102bf038439ca1af77f352164725628", + "sha256:ba69a4be8474be11720636bc2f0cf66f7054d417d4c1dbc1dfe504bb8e739541" ], "index": "pypi", - "version": "==4.3.17" + "version": "==4.3.19" }, "jedi": { "hashes": [ @@ -1306,11 +1335,11 @@ }, "mock": { "hashes": [ - "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", - "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" + "sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3", + "sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8" ], "index": "pypi", - "version": "==2.0.0" + "version": "==3.0.5" }, "packaging": { "hashes": [ @@ -1326,13 +1355,6 @@ ], "version": "==0.4.0" }, - "pbr": { - "hashes": [ - "sha256:8257baf496c8522437e8a6cfe0f15e00aedc6c0e0e7c9d55eeeeab31e0853843", - "sha256:8c361cc353d988e4f5b998555c88098b9d5964c2e11acf7b0d21925a66bb5824" - ], - "version": "==5.1.3" - }, "pdbpp": { "hashes": [ "sha256:ee7eab02ecf32d92bd66b45eedb9bda152fa13f7be0dceb7050413a52cbbc4dd" @@ -1357,10 +1379,10 @@ }, "pluggy": { "hashes": [ - "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", - "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + "sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180", + "sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a" ], - "version": "==0.9.0" + "version": "==0.11.0" }, "prompt-toolkit": { "hashes": [ @@ -1400,10 +1422,10 @@ }, "pygments": { "hashes": [ - "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", - "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + "sha256:31cba6ffb739f099a85e243eff8cb717089fdd3c7300767d9fc34cb8e1b065f5", + "sha256:5ad302949b3c98dd73f8d9fcdc7e9cb592f120e32a18e23efd7f3dc51194472b" ], - "version": "==2.3.1" + "version": "==2.4.0" }, "pyparsing": { "hashes": [ @@ -1522,11 +1544,11 @@ }, "tox": { "hashes": [ - "sha256:1b166b93d2ce66bb7b253ba944d2be89e0c9d432d49eeb9da2988b4902a4684e", - "sha256:665cbdd99f5c196dd80d1d8db8c8cf5d48b1ae1f778bccd1bdf14d5aaf4ca0fc" + "sha256:5d6b9e7ad99a93b00ecd509e13552600d38eedd2b035ba24709f850b23f51254", + "sha256:fee5b4fa2fb1638b57879a1fcaefbfd16201d8d7ecb9956406855a85d518ac4c" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.10.0" }, "traitlets": { "hashes": [ @@ -1537,17 +1559,17 @@ }, "urllib3": { "hashes": [ - "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", - "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" ], - "version": "==1.24.2" + "version": "==1.24.3" }, "virtualenv": { "hashes": [ - "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", - "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + "sha256:15ee248d13e4001a691d9583948ad3947bcb8a289775102e4c4aa98a8b7a6d73", + "sha256:bfc98bb9b42a3029ee41b96dc00a34c2f254cbf7716bec824477b2c82741a5c4" ], - "version": "==16.4.3" + "version": "==16.5.0" }, "wcwidth": { "hashes": [ diff --git a/README.rst b/README.rst index 9728cbd5c0..6ee8165e88 100644 --- a/README.rst +++ b/README.rst @@ -26,13 +26,24 @@ AGILE METHODOLOGY ----------------- The development of eTools takes on a methodology known as Agile. This methodology takes into account shot, iterative software development cycles that incorporates user feedback. +Development strategy is similar to git flow approach. +New feature and bugfix are merged into development when PR have been approved and CI passes. +Once development is completed, changes are moved to staging for QA testing. +Adjustments and fixes should go direct to staging while new features should go in development. +Once QA is completed staging branch is merged to master. MODULES ------- -eTools development follows a phased and modular approach to software development, with releases based on an agreed set of prioritized modules and features – new modules and features are released on a quarterly basis. +eTools development follows a phased and modular approach to software development, with releases based on an agreed set of prioritized modules and features – new modules and features are released on a monthly basis. -These are  modules planned for the eTools platform: +These are modules currently in production for the eTools: +* Partnership Management Portal (PMP) +* Dashboard (DASH) +* Trip Management (T2F) +* Financial Assurance Module (FAM) +* Third Party Monitoring (TPM) +* Action Point Dashboard (APD) DEVELOPMENT ROADMAP @@ -50,9 +61,7 @@ Links +--------------------+----------------+--------------+--------------------+ | Source Code |https://github.com/unicef/etools | +--------------------+----------------+-----------------------------------+ -| Issue tracker |https://github.com/unicef/etools/issues | -+--------------------+----------------+-----------------------------------+ -| Planning |https://app.clubhouse.io/unicefetools/dashboard | +| Issue tracker |https://app.clubhouse.io/unicefetools/stories | +--------------------+----------------+-----------------------------------+ @@ -63,3 +72,40 @@ Links .. |dev-cov| image:: https://circleci.com/gh/unicef/etools/tree/develop.svg?style=svg :target: https://circleci.com/gh/unicef/etools/tree/develop + + +Testing +------------------- + ++---------------------------------+--------------------------------------------------------+ +| tox | runs flake and checks there are not missing migrations | ++---------------------------------+--------------------------------------------------------+ +| tox -r | in case you want to reuse the virtualenv | ++---------------------------------+--------------------------------------------------------+ +| python manage.py test | run test related to a specific package | ++---------------------------------+--------------------------------------------------------+ + + +Environments +-------------------- ++----------------+---------------------------+-------------------------------------------------+ +| Development | etools-dev.unicef.org | - Development environment for developers | +| | | - Potentially instable | ++----------------+---------------------------+-------------------------------------------------+ +| Staging | etools-staging.unicef.org | - Staging environment for QA testing | +| | | - Release candidate | ++----------------+---------------------------+-------------------------------------------------+ +| Demo | etools-demo.unicef.org | - Demo environment | +| | | - Same version of production | +| | | - Used for demo, workshops and troubleshooting | ++----------------+---------------------------+-------------------------------------------------+ +| Test | etools-test.unicef.org | - Coming soon | ++----------------+---------------------------+-------------------------------------------------+ +| Production | etools.unicef.org | - Production environment | ++----------------+---------------------------+-------------------------------------------------+ + + +Troubleshoot +-------------------- +* Exception are logged in Sentry: https://sentry.io/unicef-jk/ +* Each container in Rancher allows to access local logs diff --git a/runtests.sh b/runtests.sh index de46b7ebd5..188a810af1 100755 --- a/runtests.sh +++ b/runtests.sh @@ -15,7 +15,7 @@ python -W ignore manage.py makemigrations --dry-run --check # Check code style unless running under tox, in which case tox runs flake8 separately if [[ $RUNNING_UNDER_TOX != 1 ]] ; then time flake8 src/ - # time isort -rc src/ --check-only + time isort -rc src/ --check-only fi # Run unittests and coverage report diff --git a/setup.py b/setup.py index 7df772b5a6..808b294b7f 100644 --- a/setup.py +++ b/setup.py @@ -41,5 +41,7 @@ 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', + 'Framework :: Django', + 'Framework :: Django :: 2.1', ], ) diff --git a/src/etools/__init__.py b/src/etools/__init__.py index 5e6d14a902..347573ab39 100644 --- a/src/etools/__init__.py +++ b/src/etools/__init__.py @@ -1,2 +1,2 @@ -VERSION = __version__ = '6.10' +VERSION = __version__ = '7.0' NAME = 'eTools' diff --git a/src/etools/applications/action_points/export/renderers.py b/src/etools/applications/action_points/export/renderers.py index 65df562361..047d8a1312 100644 --- a/src/etools/applications/action_points/export/renderers.py +++ b/src/etools/applications/action_points/export/renderers.py @@ -6,12 +6,13 @@ class ActionPointCSVRenderer(ListSeperatorCSVRenderMixin, FriendlyCSVRenderer): header = [ - 'ref', 'cp_output', 'partner', 'office', 'section', 'category', 'assigned_to', 'due_date', + 'ref', 'ref_link', 'cp_output', 'partner', 'office', 'section', 'category', 'assigned_to', 'due_date', 'status', 'high_priority', 'description', 'intervention', 'pd_ssfa', 'location', 'related_module', - 'assigned_by', 'date_of_completion', 'related_ref', 'related_object_str', 'related_object_url', 'action_taken' + 'assigned_by', 'date_of_completion', 'related_ref', 'related_object_str', 'related_object_url', 'action_taken', ] labels = { 'ref': _('Ref. #'), + 'ref_link': _('Link'), 'cp_output': _('CP Output'), 'partner': _('Partner'), 'office': _('Office'), diff --git a/src/etools/applications/action_points/export/serializers.py b/src/etools/applications/action_points/export/serializers.py index 22944e3c6c..2f73b8ebd4 100644 --- a/src/etools/applications/action_points/export/serializers.py +++ b/src/etools/applications/action_points/export/serializers.py @@ -4,6 +4,7 @@ class ActionPointExportSerializer(serializers.Serializer): ref = serializers.CharField(source='reference_number', read_only=True) + ref_link = serializers.SerializerMethodField() cp_output = serializers.CharField(allow_null=True) partner = serializers.CharField(source='partner.name', allow_null=True) office = serializers.CharField(source='office.name', allow_null=True) @@ -28,3 +29,6 @@ class ActionPointExportSerializer(serializers.Serializer): def get_action_taken(self, obj): return ";\n\n".join(["{} ({}): {}".format(c.user if c.user else '-', c.submit_date.strftime( "%d %b %Y"), c.comment) for c in obj.comments.all()]) + + def get_ref_link(self, obj): + return obj.get_object_url() diff --git a/src/etools/applications/action_points/migrations/0009_auto_20190523_1146.py b/src/etools/applications/action_points/migrations/0009_auto_20190523_1146.py new file mode 100644 index 0000000000..a11c6190f6 --- /dev/null +++ b/src/etools/applications/action_points/migrations/0009_auto_20190523_1146.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2019-05-23 11:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('action_points', '0008_auto_20180731_1050'), + ] + + operations = [ + migrations.AlterField( + model_name='actionpoint', + name='travel_activity', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='t2f.TravelActivity', verbose_name='Travel Activity'), + ), + ] diff --git a/src/etools/applications/action_points/models.py b/src/etools/applications/action_points/models.py index 669d56c004..9f4768962f 100644 --- a/src/etools/applications/action_points/models.py +++ b/src/etools/applications/action_points/models.py @@ -85,7 +85,7 @@ class ActionPoint(TimeStampedModel): tpm_activity = models.ForeignKey('tpm.TPMActivity', verbose_name=_('TPM Activity'), blank=True, null=True, related_name='action_points', on_delete=models.CASCADE, ) - travel_activity = models.ForeignKey('t2f.TravelActivity', verbose_name=_('Travel'), blank=True, null=True, + travel_activity = models.ForeignKey('t2f.TravelActivity', verbose_name=_('Travel Activity'), blank=True, null=True, on_delete=models.CASCADE, ) diff --git a/src/etools/applications/action_points/serializers.py b/src/etools/applications/action_points/serializers.py index cc3f6affad..41ba3a12f3 100644 --- a/src/etools/applications/action_points/serializers.py +++ b/src/etools/applications/action_points/serializers.py @@ -4,7 +4,6 @@ from django_comments.models import Comment from rest_framework import serializers from unicef_locations.serializers import LocationLightSerializer -from unicef_rest_export.serializers import ExportSerializer from unicef_restlib.fields import SeparatedReadWriteField from unicef_restlib.serializers import UserContextSerializerMixin, WritableNestedSerializerMixin from unicef_snapshot.models import Activity @@ -181,98 +180,3 @@ def validate_category(self, value): if value and value.module != self.instance.related_module: raise serializers.ValidationError(_('Category doesn\'t belong to selected module.')) return value - - -class ActionPointListExportSerializer(ExportSerializer): - engagement = serializers.SerializerMethodField() - tpm_activity = serializers.SerializerMethodField() - travel_activity = serializers.SerializerMethodField() - - class Meta(ActionPointListSerializer.Meta): - pass - - def get_engagement(self, obj): - return obj.engagement - - def get_tpm_activity(self, obj): - return obj.tpm_activity - - def get_travel_activity(self, obj): - return obj.travel_activity - - def get_headers(self, data): - headers = [] - for field in data[0].keys(): - headers.append(str(self.get_header_label(field))) - return headers - - def transform_cp_output(self, data): - if data is None: - return data - return data.get("result_name", "") - - def transform_location(self, data): - if data is None: - return data - return data.get("name", "") - - def transform_office(self, data): - if data is None: - return data - return data.get("name", "") - - def transform_section(self, data): - if data is None: - return data - return data.get("name", "") - - def transform_author(self, data): - return "{} {}".format( - data.get("first_name", ""), - data.get("last_name", "") - ) - - def transform_assigned_by(self, data): - return "{} {}".format( - data.get("first_name", ""), - data.get("last_name", "") - ) - - def transform_assigned_to(self, data): - return "{} {}".format( - data.get("first_name", ""), - data.get("last_name", "") - ) - - def transform_partner(self, data): - if data is None: - return data - return data.get("name", "") - - def transform_intervention(self, data): - if data is None: - return data - return data.get("number", "") - - def transform_category(self, data): - if data is None: - return data - return data.get("module", "") - - def transform_dataset(self, dataset): - transform_fields = [ - 'category', - 'assigned_to', - 'author', - 'assigned_by', - 'section', - 'office', - 'cp_output', - 'partner', - 'intervention', - 'location', - ] - for field in transform_fields: - func = getattr(self, "transform_{}".format(field)) - dataset.add_formatter(str(self.get_header_label(field)), func) - return dataset diff --git a/src/etools/applications/action_points/tests/base.py b/src/etools/applications/action_points/tests/base.py index 9938e0cfd8..d9c6a015e2 100644 --- a/src/etools/applications/action_points/tests/base.py +++ b/src/etools/applications/action_points/tests/base.py @@ -1,7 +1,7 @@ from etools.libraries.djangolib.models import GroupWrapper -class ActionPointsTestCaseMixin(object): +class ActionPointsTestCaseMixin: def setUp(self): super().setUp() diff --git a/src/etools/applications/action_points/views.py b/src/etools/applications/action_points/views.py index ff809f7963..9190a8c059 100644 --- a/src/etools/applications/action_points/views.py +++ b/src/etools/applications/action_points/views.py @@ -7,6 +7,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from unicef_rest_export.renderers import ExportOpenXMLRenderer +from unicef_rest_export.serializers import ExportSerializer from unicef_rest_export.views import ExportMixin from unicef_restlib.pagination import DynamicPageNumberPagination from unicef_restlib.views import MultiSerializerViewSetMixin, SafeTenantViewSetMixin @@ -28,7 +29,6 @@ from etools.applications.action_points.models import ActionPoint from etools.applications.action_points.serializers import ( ActionPointCreateSerializer, - ActionPointListExportSerializer, ActionPointListSerializer, ActionPointSerializer, ) @@ -67,7 +67,7 @@ class ActionPointViewSet( 'create': ActionPointCreateSerializer, 'list': ActionPointListSerializer, } - export_serializer_class = ActionPointListExportSerializer + export_serializer_class = ExportSerializer filter_backends = (ReferenceNumberOrderingFilter, OrderingFilter, SearchFilter, RelatedModuleFilter, DjangoFilterBackend,) @@ -111,7 +111,11 @@ def get_obj_permission_context(self, obj): @action(detail=False, methods=['get'], url_path='export/csv', renderer_classes=(ActionPointCSVRenderer,)) def list_csv_export(self, request, *args, **kwargs): action_points = self.filter_queryset(self.get_queryset().prefetch_related('comments')) - serializer = ActionPointExportSerializer(action_points.prefetch_related('comments'), many=True) + serializer = ActionPointExportSerializer( + action_points.prefetch_related('comments'), + context={"request": request}, + many=True, + ) return Response(serializer.data, headers={ 'Content-Disposition': 'attachment;filename=action_points_{}.csv'.format(timezone.now().date()) @@ -119,7 +123,7 @@ def list_csv_export(self, request, *args, **kwargs): @action(detail=False, methods=['get'], url_path='export/xlsx', renderer_classes=(ExportOpenXMLRenderer,)) def list_xlsx_export(self, request, *args, **kwargs): - self.serializer_class = ActionPointListSerializer + self.serializer_class = ActionPointExportSerializer action_points = self.filter_queryset(self.get_queryset().prefetch_related('comments')) serializer = self.get_serializer(action_points, many=True) return Response(serializer.data, headers={ @@ -128,7 +132,10 @@ def list_xlsx_export(self, request, *args, **kwargs): @action(detail=True, methods=['get'], url_path='export/csv', renderer_classes=(ActionPointCSVRenderer,)) def single_csv_export(self, request, *args, **kwargs): - serializer = ActionPointExportSerializer(self.get_object()) + serializer = ActionPointExportSerializer( + self.get_object(), + context={"request": request}, + ) return Response(serializer.data, headers={ 'Content-Disposition': 'attachment;filename={}_{}.csv'.format( self.get_object().reference_number, timezone.now().date() @@ -137,7 +144,7 @@ def single_csv_export(self, request, *args, **kwargs): @action(detail=True, methods=['get'], url_path='export/xlsx', renderer_classes=(ExportOpenXMLRenderer,)) def single_xlsx_export(self, request, *args, **kwargs): - self.serializer_class = ActionPointListSerializer + self.serializer_class = ActionPointExportSerializer serializer = self.get_serializer([self.get_object()], many=True) return Response(serializer.data, headers={ 'Content-Disposition': 'attachment;filename={}_{}.xlsx'.format( diff --git a/src/etools/applications/attachments/management/commands/init-attachment-file-types.py b/src/etools/applications/attachments/management/commands/init-attachment-file-types.py new file mode 100644 index 0000000000..803eede4e4 --- /dev/null +++ b/src/etools/applications/attachments/management/commands/init-attachment-file-types.py @@ -0,0 +1,59 @@ +import logging + +from django.core.management.base import BaseCommand +from django.db import connection + +from unicef_attachments.models import FileType + +from etools.applications.users.models import Country +from etools.libraries.tenant_support.utils import run_on_all_tenants + +logger = logging.getLogger(__name__) + +FILE_TYPES_MAPPING = [ + # ("code", "label", "name", "order"), + ("partners_agreement", "Signed Agreement", "attached_agreement", 0), + ("partners_partner_assessment", "Core Values Assessment", "core_values_assessment", 0), + ("partners_assessment_report", "Assessment Report", "assessment_report", 0), + ("partners_agreement_amendment", "Agreement Amendment", "agreement_signed_amendment", 0), + ("partners_intervention_prc_review", "PRC Review", "intervention_prc_review", 0), + ("partners_intervention_signed_pd", "Signed PD/SSFA", "intervention_signed_pd", 0), + ("partners_intervention_activation_letter", "PD Activation Letter", "activation_letter", 0), + ("partners_intervention_termination_doc", "PD Termination Document", "termination_doc", 0), + ("partners_intervention_amendment_signed", "PD/SSFA Amendment", "intervention_amendment_signed", 0), + ("partners_intervention_attachment", "Intervention Attachment", "intervention_attachment", 0), + ("t2f_travel_attachment", "Travel Attachment", "t2f_travel_attachment", 0), +] + + +class Command(BaseCommand): + help = 'Init Attachment File Type command' + + def add_arguments(self, parser): + parser.add_argument('--schema', dest='schema') + + def run(self): + logger.info('Initialization for %s' % connection.schema_name) + for code, label, name, order in FILE_TYPES_MAPPING: + FileType.objects.update_or_create( + code=code, + defaults={ + "label": label, + "name": name, + "order": order, + } + ) + + def handle(self, *args, **options): + + logger.info('Command started') + + countries = Country.objects.exclude(name__iexact='global') + if options['schema']: + country = countries.get(schema_name=options['schema']) + connection.set_tenant(country) + self.run() + else: + run_on_all_tenants(self.run) + + logger.info('Command finished') diff --git a/src/etools/applications/audit/purchase_order/admin.py b/src/etools/applications/audit/purchase_order/admin.py index 0c73fd5e58..0b9a0727e9 100644 --- a/src/etools/applications/audit/purchase_order/admin.py +++ b/src/etools/applications/audit/purchase_order/admin.py @@ -48,6 +48,11 @@ class PurchaseOrderAdmin(admin.ModelAdmin): @admin.register(AuditorStaffMember) class AuditorStaffAdmin(admin.ModelAdmin): - list_display = ['user', 'auditor_firm', 'hidden'] + list_display = ['user', 'email', 'auditor_firm', 'hidden'] list_filter = ['auditor_firm', 'hidden'] search_fields = ['user__username', 'user__email', 'user__first_name', 'user__last_name', 'auditor_firm__name', ] + + def email(self, obj): + return obj.user.email + + email.admin_order_field = 'user__email' diff --git a/src/etools/applications/audit/purchase_order/synchronizers.py b/src/etools/applications/audit/purchase_order/synchronizers.py index 65ff6d1cc2..bdc5f17925 100644 --- a/src/etools/applications/audit/purchase_order/synchronizers.py +++ b/src/etools/applications/audit/purchase_order/synchronizers.py @@ -1,7 +1,8 @@ from collections import OrderedDict +from unicef_vision.synchronizers import ManualVisionSynchronizer + from etools.applications.audit.purchase_order.models import AuditorFirm, PurchaseOrder, PurchaseOrderItem -from etools.applications.vision.synchronizers import ManualVisionSynchronizer class POSynchronizer(ManualVisionSynchronizer): diff --git a/src/etools/applications/audit/purchase_order/tasks.py b/src/etools/applications/audit/purchase_order/tasks.py index 27b9490e55..ffbdbb031f 100644 --- a/src/etools/applications/audit/purchase_order/tasks.py +++ b/src/etools/applications/audit/purchase_order/tasks.py @@ -1,11 +1,11 @@ from celery.utils.log import get_task_logger - # Not scheduled by any code in this repo, but by other means, so keep it around. # Continues on to the next country on any VisionException, so no need to have # celery retry it in that case. +from unicef_vision.exceptions import VisionException + from etools.applications.audit.purchase_order.synchronizers import POSynchronizer from etools.applications.users.models import Country -from etools.applications.vision.exceptions import VisionException from etools.config.celery import app logger = get_task_logger(__name__) diff --git a/src/etools/applications/audit/purchase_order/tests/test_synchronizers.py b/src/etools/applications/audit/purchase_order/tests/test_synchronizers.py index 3bb1a1e103..9aa1e98e0d 100644 --- a/src/etools/applications/audit/purchase_order/tests/test_synchronizers.py +++ b/src/etools/applications/audit/purchase_order/tests/test_synchronizers.py @@ -26,15 +26,15 @@ def setUp(self): "GRANT_REF": "Grantor", "PO_ITEM": "456", } - self.adapter = synchronizers.POSynchronizer(self.country) + self.adapter = synchronizers.POSynchronizer(self.country.business_area_code) def test_init_no_object_number(self): - a = synchronizers.POSynchronizer(self.country) - self.assertEqual(a.country, self.country) + a = synchronizers.POSynchronizer(self.country.business_area_code) + self.assertEqual(a.business_area_code, self.country.business_area_code) def test_init(self): - a = synchronizers.POSynchronizer(self.country, object_number="123") - self.assertEqual(a.country, self.country) + a = synchronizers.POSynchronizer(self.country.business_area_code, object_number="123") + self.assertEqual(a.business_area_code, self.country.business_area_code) def test_convert_records_list(self): """Ensure list is not touched""" diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 3cb8d8c267..1d5c4fa75c 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -282,7 +282,7 @@ def validate(self, data): return validated_data -class ActivePDValidationMixin(object): +class ActivePDValidationMixin: def validate(self, data): validated_data = super().validate(data) diff --git a/src/etools/applications/audit/serializers/mixins.py b/src/etools/applications/audit/serializers/mixins.py index 2dba23a982..e6917868c2 100644 --- a/src/etools/applications/audit/serializers/mixins.py +++ b/src/etools/applications/audit/serializers/mixins.py @@ -7,7 +7,7 @@ from etools.applications.audit.serializers.risks import RiskRootSerializer -class RiskCategoriesUpdateMixin(object): +class RiskCategoriesUpdateMixin: """ Mixin that allow to update risk values through engagement serializer. """ @@ -31,7 +31,7 @@ def update(self, instance, validated_data): return instance -class EngagementDatesValidation(object): +class EngagementDatesValidation: date_fields = [ 'partner_contacted_at', 'date_of_draft_report_to_ip', 'date_of_comments_by_ip', diff --git a/src/etools/applications/audit/signals.py b/src/etools/applications/audit/signals.py index 518ef1f172..07d35de7c3 100644 --- a/src/etools/applications/audit/signals.py +++ b/src/etools/applications/audit/signals.py @@ -1,20 +1,12 @@ - from django.db import connection from django.db.models.signals import m2m_changed, post_save from django.dispatch import receiver -from etools.applications.audit.models import Auditor, Engagement, EngagementActionPoint +from etools.applications.audit.models import Engagement, EngagementActionPoint from etools.applications.audit.purchase_order.models import AuditorStaffMember from etools.applications.users.models import Country -@receiver(post_save, sender=AuditorStaffMember) -def create_user_receiver(instance, created, **kwargs): - if created: - instance.user.groups.add(Auditor.as_group()) - instance.user.profile.countries_available.add(connection.tenant) - - @receiver(m2m_changed, sender=Engagement.staff_members.through) def staff_member_changed(sender, instance, action, reverse, pk_set, *args, **kwargs): if action == 'post_add': diff --git a/src/etools/applications/audit/tests/base.py b/src/etools/applications/audit/tests/base.py index a32c7ccbb6..3a07e38bc0 100644 --- a/src/etools/applications/audit/tests/base.py +++ b/src/etools/applications/audit/tests/base.py @@ -24,7 +24,7 @@ from etools.libraries.djangolib.models import GroupWrapper -class AuditTestCaseMixin(object): +class AuditTestCaseMixin: @classmethod def setUpTestData(cls): super().setUpTestData() diff --git a/src/etools/applications/audit/tests/test_models.py b/src/etools/applications/audit/tests/test_models.py index bdf664af5e..b1839052ef 100644 --- a/src/etools/applications/audit/tests/test_models.py +++ b/src/etools/applications/audit/tests/test_models.py @@ -9,8 +9,8 @@ from django.db import connection from django.test import SimpleTestCase -from etools.applications.audit.models import Auditor, Engagement, RiskCategory -from etools.applications.audit.purchase_order.models import AuditorStaffMember, PurchaseOrder, PurchaseOrderItem +from etools.applications.audit.models import Engagement, RiskCategory +from etools.applications.audit.purchase_order.models import PurchaseOrder, PurchaseOrderItem from etools.applications.audit.tests.factories import ( AuditFactory, AuditorStaffMemberFactory, @@ -32,17 +32,6 @@ from etools.applications.users.models import Country -class AuditorStaffMemberTestCase(BaseTenantTestCase): - def test_signal(self): - self.firm = AuditPartnerFactory() - user = BaseUserFactory() - Auditor.invalidate_cache() - - staff_member = AuditorStaffMember.objects.create(auditor_firm=self.firm, user=user) - - self.assertIn(Auditor.name, staff_member.user.groups.values_list('name', flat=True)) - - class EngagementStaffMemberTestCase(BaseTenantTestCase): @classmethod diff --git a/src/etools/applications/audit/tests/test_transitions.py b/src/etools/applications/audit/tests/test_transitions.py index fc3bc83470..8ca9c75070 100644 --- a/src/etools/applications/audit/tests/test_transitions.py +++ b/src/etools/applications/audit/tests/test_transitions.py @@ -24,7 +24,7 @@ from etools.applications.core.tests.cases import BaseTenantTestCase -class EngagementCheckTransitionsTestCaseMixin(object): +class EngagementCheckTransitionsTestCaseMixin: fixtures = ('audit_risks_blueprints', ) def _test_transition(self, user, action, expected_response, errors=None, data=None): @@ -252,7 +252,7 @@ def test_cancel_finalized_focal_point(self): self._test_cancel(self.unicef_focal_point, status.HTTP_403_FORBIDDEN) -class EngagementCheckTransitionsMetadataTestCaseMixin(object): +class EngagementCheckTransitionsMetadataTestCaseMixin: def _test_allowed_actions(self, user, actions): response = self.forced_auth_req( 'options', self.engagement_url(), user=user diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index d53b0329bf..c22c0bfcee 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -458,7 +458,7 @@ def _do_create(self, user, data): return response -class TestEngagementCreateActivePDViewSet(object): +class TestEngagementCreateActivePDViewSet: def test_partner_without_active_pd(self): del self.create_data['active_pd'] diff --git a/src/etools/applications/audit/transitions/conditions.py b/src/etools/applications/audit/transitions/conditions.py index 5ee6f7502f..56d38600b1 100644 --- a/src/etools/applications/audit/transitions/conditions.py +++ b/src/etools/applications/audit/transitions/conditions.py @@ -9,7 +9,7 @@ from rest_framework.serializers import ValidationError -class BaseTransitionCheck(object): +class BaseTransitionCheck: def get_errors(self, *args, **kwargs): return {} diff --git a/src/etools/applications/core/migrations/0003_auto_20190424_1448.py b/src/etools/applications/core/migrations/0003_auto_20190424_1448.py new file mode 100644 index 0000000000..a198600221 --- /dev/null +++ b/src/etools/applications/core/migrations/0003_auto_20190424_1448.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.8 on 2019-04-24 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_auto_20190312_1437'), + ] + + operations = [ + migrations.AlterField( + model_name='domain', + name='is_primary', + field=models.BooleanField(db_index=True, default=True), + ), + ] diff --git a/src/etools/applications/core/mixins.py b/src/etools/applications/core/mixins.py index f817a1b9aa..eaff9a17ff 100644 --- a/src/etools/applications/core/mixins.py +++ b/src/etools/applications/core/mixins.py @@ -3,7 +3,7 @@ from rest_framework import serializers -class ExportModelMixin(object): +class ExportModelMixin: def set_labels(self, serializer_fields, model): labels = {} model_labels = {} @@ -32,7 +32,7 @@ def get_renderer_context(self): return context -class ExportSerializerMixin(object): +class ExportSerializerMixin: country = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): diff --git a/src/etools/applications/core/permissions.py b/src/etools/applications/core/permissions.py index 7d4bc75896..73c095de67 100644 --- a/src/etools/applications/core/permissions.py +++ b/src/etools/applications/core/permissions.py @@ -81,6 +81,10 @@ def process_file(): return result cache_key = "public-{}-permissions".format(model_name.lower()) - response = cache.get_or_set(cache_key, process_file, 60 * 60 * 24) + response = cache.get(cache_key, None) + + if response is None: + response = process_file() + cache.set(cache_key, response) return response diff --git a/src/etools/applications/core/renderers.py b/src/etools/applications/core/renderers.py index 72fbe03ea4..d3d21b53a8 100644 --- a/src/etools/applications/core/renderers.py +++ b/src/etools/applications/core/renderers.py @@ -17,7 +17,7 @@ def flatten_item(self, item): return super().flatten_item(item) -class ListSeperatorCSVRenderMixin(object): +class ListSeperatorCSVRenderMixin: """Mixin which render list concatenating them usign the separator""" separator = '\n\n' diff --git a/src/etools/applications/core/tests/cases.py b/src/etools/applications/core/tests/cases.py index 7c9033c293..8677f67ad7 100644 --- a/src/etools/applications/core/tests/cases.py +++ b/src/etools/applications/core/tests/cases.py @@ -58,17 +58,10 @@ def setUpClass(cls): EmailTemplate.objects.get_or_create(name='audit/engagement/submit_to_auditor') TenantModel = get_tenant_model() - try: - cls.tenant = TenantModel.objects.get(schema_name=SCHEMA_NAME) - except TenantModel.DoesNotExist: - cls.tenant = TenantModel(schema_name=SCHEMA_NAME) - cls.tenant.save(verbosity=0) - - cls.tenant.business_area_code = 'ZZZ' - # Make sure country has a short code, it affects some results - cls.tenant.country_short_code = 'TST' - cls.tenant.save(verbosity=0) - + cls.tenant, _ = TenantModel.objects.get_or_create(schema_name=SCHEMA_NAME, defaults={ + 'business_area_code': 'ZZZ', + 'country_short_code': 'TST' + }) cls.domain = get_tenant_domain_model().objects.get_or_create(domain=TENANT_DOMAIN, tenant=cls.tenant) try: diff --git a/src/etools/applications/core/tests/mixins.py b/src/etools/applications/core/tests/mixins.py index 82a4f4eb4d..66159acf06 100644 --- a/src/etools/applications/core/tests/mixins.py +++ b/src/etools/applications/core/tests/mixins.py @@ -9,7 +9,7 @@ def _delimit_namespace(namespace): return namespace -class URLAssertionMixin(object): +class URLAssertionMixin: """Mixin for any class derived from unittest.TestCase. Provides some assertion helpers for testing URL patterns""" def assertReversal(self, names_and_paths, namespace, url_prefix): @@ -74,7 +74,7 @@ def assertIntParamRegexes(self, names_and_paths, namespace): raise AssertionError(fail_msg) -class WorkspaceRequiredAPITestMixIn(object): +class WorkspaceRequiredAPITestMixIn: """ For BaseTenantTestCases that have a required workspace param, just automatically set the current tenant. diff --git a/src/etools/applications/firms/serializers.py b/src/etools/applications/firms/serializers.py index 5a783121cf..8f0574d1fc 100644 --- a/src/etools/applications/firms/serializers.py +++ b/src/etools/applications/firms/serializers.py @@ -5,7 +5,6 @@ from rest_framework.validators import UniqueValidator from unicef_restlib.serializers import WritableNestedSerializerMixin -from etools.applications.firms.utils import generate_username from etools.applications.users.models import UserProfile @@ -26,7 +25,9 @@ class Meta(WritableNestedSerializerMixin.Meta): class UserSerializer(WritableNestedSerializerMixin, serializers.ModelSerializer): profile = UserProfileSerializer(required=False) email = serializers.EmailField( - label=_('E-mail Address'), validators=[UniqueValidator(queryset=get_user_model().objects.all())] + label=_('E-mail Address'), + validators=[UniqueValidator(queryset=get_user_model().objects.all(), + message='This user already exists in the system')] ) class Meta(WritableNestedSerializerMixin.Meta): @@ -38,7 +39,8 @@ class Meta(WritableNestedSerializerMixin.Meta): } def create(self, validated_data): - validated_data.setdefault('username', generate_username()) + email = validated_data.get('email', None) + validated_data.setdefault('username', email) return super().create(validated_data) diff --git a/src/etools/applications/firms/tests/factories.py b/src/etools/applications/firms/tests/factories.py index 8e51a641be..5b264d3f48 100644 --- a/src/etools/applications/firms/tests/factories.py +++ b/src/etools/applications/firms/tests/factories.py @@ -5,7 +5,6 @@ import factory -from etools.applications.firms.utils import generate_username from etools.applications.users.tests.factories import ProfileFactory @@ -15,7 +14,7 @@ class Meta: model = get_user_model() django_get_or_create = ("email", ) - username = factory.LazyFunction(generate_username) + username = factory.Faker('email') first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') email = factory.Faker('email') diff --git a/src/etools/applications/firms/tests/test_utils.py b/src/etools/applications/firms/tests/test_utils.py deleted file mode 100644 index 2490f26791..0000000000 --- a/src/etools/applications/firms/tests/test_utils.py +++ /dev/null @@ -1,22 +0,0 @@ - -from django.test import SimpleTestCase - -from etools.applications.firms.utils import generate_username - - -class UsernameGeneratorTestCase(SimpleTestCase): - iterations = 10 ** 5 - - def test_length(self): - for _ in range(self.iterations): - username = generate_username() - self.assertLessEqual(len(username), 30, "`%s` longer then %s" % (username, 30)) - - def test_collision(self): - usernames = set() - for _ in range(self.iterations): - username = generate_username() - - self.assertNotIn(username, usernames) - - usernames.add(username) diff --git a/src/etools/applications/firms/utils.py b/src/etools/applications/firms/utils.py index 7bc271d153..58a21066b1 100644 --- a/src/etools/applications/firms/utils.py +++ b/src/etools/applications/firms/utils.py @@ -1,6 +1,3 @@ -import string -import uuid - from django.urls import reverse from unicef_notification.utils import send_notification_with_template @@ -8,21 +5,6 @@ from etools.libraries.djangolib.utils import get_environment -def generate_username(): - base = 32 - abc_function = (string.digits + string.ascii_lowercase)[:base] - - uid = uuid.uuid4().int - digits = [] - while uid: - digits.append(abc_function[uid % base]) - uid //= base - - digits.reverse() - uid = ''.join(digits) - return '-'.join([uid[:6], uid[6:10], uid[10:16], uid[16:20], uid[20:]]) - - def send_invite_email(staff): context = { 'environment': get_environment(), diff --git a/src/etools/applications/funds/migrations/0010_fundsreservationheader_completed_flag.py b/src/etools/applications/funds/migrations/0010_fundsreservationheader_completed_flag.py new file mode 100644 index 0000000000..48501a6518 --- /dev/null +++ b/src/etools/applications/funds/migrations/0010_fundsreservationheader_completed_flag.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2019-05-23 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0009_auto_20180515_1720'), + ] + + operations = [ + migrations.AddField( + model_name='fundsreservationheader', + name='completed_flag', + field=models.BooleanField(default=False, verbose_name='FR marked as completed in Vision'), + ), + ] diff --git a/src/etools/applications/funds/models.py b/src/etools/applications/funds/models.py index d6be3b3ba7..47665cd9c9 100644 --- a/src/etools/applications/funds/models.py +++ b/src/etools/applications/funds/models.py @@ -166,6 +166,10 @@ class FundsReservationHeader(TimeStampedModel): default=False, verbose_name=_("Actual and DCT in various currencies"), ) + completed_flag = models.BooleanField( + default=False, + verbose_name=_("FR marked as completed in Vision") + ) def __str__(self): return '{}'.format( diff --git a/src/etools/applications/funds/synchronizers.py b/src/etools/applications/funds/synchronizers.py index 36522c2628..01c0965e06 100644 --- a/src/etools/applications/funds/synchronizers.py +++ b/src/etools/applications/funds/synchronizers.py @@ -5,19 +5,35 @@ from django.db.models import Sum +from unicef_vision.synchronizers import FileDataSynchronizer +from unicef_vision.utils import comp_decimals + from etools.applications.funds.models import ( FundsCommitmentHeader, FundsCommitmentItem, FundsReservationHeader, FundsReservationItem, ) -from etools.applications.vision.utils import comp_decimals -from etools.applications.vision.vision_data_synchronizer import FileDataSynchronizer, VisionDataSynchronizer +from etools.applications.vision.synchronizers import VisionDataTenantSynchronizer -class FundReservationsSynchronizer(VisionDataSynchronizer): +class FundReservationsSynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetFundsReservationInfo_JSON' + REQUIRED_NON_NULL_VALUE_KEYS = ( + "VENDOR_CODE", + "FR_NUMBER", + "FR_DOC_DATE", + "CURRENCY", + "FR_DOCUMENT_TEXT", + "FR_START_DATE", + "FR_END_DATE", + "LINE_ITEM", + "WBS_ELEMENT", + "DUE_DATE", + "FUND", + "GRANT_NBR", + ) REQUIRED_KEYS = ( "VENDOR_CODE", "FR_NUMBER", @@ -41,7 +57,8 @@ class FundReservationsSynchronizer(VisionDataSynchronizer): "OUTSTANDING_DCT", 'ACTUAL_CASH_TRANSFER_DC', 'OUTSTANDING_DCT_DC', - 'MULTI_CURR_FLAG' + 'MULTI_CURR_FLAG', + 'COMPLETED_FLAG' ) MAPPING = { "vendor_code": "VENDOR_CODE", @@ -68,12 +85,13 @@ class FundReservationsSynchronizer(VisionDataSynchronizer): "actual_amt_local": "ACTUAL_CASH_TRANSFER_DC", "outstanding_amt": "OUTSTANDING_DCT", "outstanding_amt_local": "OUTSTANDING_DCT_DC", - "multi_curr_flag": "MULTI_CURR_FLAG" + "multi_curr_flag": "MULTI_CURR_FLAG", + "completed_flag": "COMPLETED_FLAG" } HEADER_FIELDS = ['VENDOR_CODE', 'FR_NUMBER', 'FR_DOC_DATE', 'FR_TYPE', 'CURRENCY', 'FR_DOCUMENT_TEXT', 'FR_START_DATE', 'FR_END_DATE', "FR_OVERALL_AMOUNT", "CURRENT_FR_AMOUNT", "ACTUAL_CASH_TRANSFER", "OUTSTANDING_DCT", - 'ACTUAL_CASH_TRANSFER_DC', 'OUTSTANDING_DCT_DC', 'MULTI_CURR_FLAG'] + 'ACTUAL_CASH_TRANSFER_DC', 'OUTSTANDING_DCT_DC', 'MULTI_CURR_FLAG', "COMPLETED_FLAG"] LINE_ITEM_FIELDS = ['LINE_ITEM', 'FR_NUMBER', 'WBS_ELEMENT', 'GRANT_NBR', 'FUND', 'OVERALL_AMOUNT', 'OVERALL_AMOUNT_DC', @@ -88,22 +106,37 @@ def __init__(self, *args, **kwargs): self.REVERSE_ITEM_FIELDS = [self.REVERSE_MAPPING[v] for v in self.LINE_ITEM_FIELDS] super().__init__(*args, **kwargs) + def _fill_required_keys(self, record): + for req_key in self.REQUIRED_KEYS: + try: + record[req_key] + except KeyError: + record[req_key] = None + def _convert_records(self, records): - return json.loads(records)["ROWSET"]["ROW"] + json_records = json.loads(records)["ROWSET"]["ROW"] + for r in json_records: + self._fill_required_keys(r) + return json_records def map_header_objects(self, qs): for item in qs: self.fr_headers[item.fr_number] = item def _filter_records(self, records): - records = super()._filter_records(records) + if "ROWSET" in records: + records = records["ROWSET"] + if "ROW" in records: + records = records["ROW"] def bad_record(record): # We don't care about FRs without expenditure if not record['OVERALL_AMOUNT']: return False - if not record['FR_NUMBER']: - return False + + for key in self.REQUIRED_NON_NULL_VALUE_KEYS: + if record[key] is None or record[key] == "": + return False return True return [rec for rec in records if bad_record(rec)] @@ -113,7 +146,9 @@ def get_value_for_field(field, value): if field in ['start_date', 'end_date', 'document_date', 'due_date']: return datetime.datetime.strptime(value, '%d-%b-%y').date() if field == 'multi_curr_flag': - return value != 'N' + return value is not None and value != 'N' + if field == 'completed_flag': + return value is not None and value != 'N' return value def get_fr_item_number(self, record): @@ -163,13 +198,11 @@ def header_sync(self): to_update = [] fr_numbers_from_records = {k for k in self.header_records.keys()} - list_of_headers = FundsReservationHeader.objects.filter(fr_number__in=fr_numbers_from_records) for h in list_of_headers: if h.fr_number in fr_numbers_from_records: to_update.append(h) fr_numbers_from_records.remove(h.fr_number) - to_create = [] for item in fr_numbers_from_records: record = self.header_records[item] @@ -251,7 +284,7 @@ def _save_records(self, records): return processed -class FundCommitmentSynchronizer(VisionDataSynchronizer): +class FundCommitmentSynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetFundsCommitmentInfo_JSON' REQUIRED_KEYS = ( @@ -401,9 +434,6 @@ def header_sync(self): if to_create: created_objects = FundsCommitmentHeader.objects.bulk_create(to_create) - # TODO in Django 1.10 the following line is not needed because ids are returned - created_objects = FundsCommitmentHeader.objects.filter( - fc_number__in=[c.fc_number for c in created_objects]) self.map_header_objects(created_objects) self.map_header_objects(to_update) diff --git a/src/etools/applications/funds/tests/test_api.py b/src/etools/applications/funds/tests/test_api.py index 30d9848d60..c13c0b64b9 100644 --- a/src/etools/applications/funds/tests/test_api.py +++ b/src/etools/applications/funds/tests/test_api.py @@ -62,8 +62,8 @@ def test_csv_export_api(self): self.assertEqual(response.status_code, status.HTTP_200_OK) dataset = Dataset().load(response.content.decode('utf-8'), 'csv') self.assertEqual(dataset.height, 1) - self.assertEqual(len(dataset._get_headers()), 20) - self.assertEqual(len(dataset[0]), 20) + self.assertEqual(len(dataset._get_headers()), 21) + self.assertEqual(len(dataset[0]), 21) def test_csv_flat_export_api(self): response = self.forced_auth_req( @@ -76,8 +76,8 @@ def test_csv_flat_export_api(self): self.assertEqual(response.status_code, status.HTTP_200_OK) dataset = Dataset().load(response.content.decode('utf-8'), 'csv') self.assertEqual(dataset.height, 1) - self.assertEqual(len(dataset._get_headers()), 20) - self.assertEqual(len(dataset[0]), 20) + self.assertEqual(len(dataset._get_headers()), 21) + self.assertEqual(len(dataset[0]), 21) class TestFundsReservationItemExportList(BaseTenantTestCase): diff --git a/src/etools/applications/funds/tests/test_synchronizers.py b/src/etools/applications/funds/tests/test_synchronizers.py index 741d152baf..98d59bf424 100644 --- a/src/etools/applications/funds/tests/test_synchronizers.py +++ b/src/etools/applications/funds/tests/test_synchronizers.py @@ -51,7 +51,8 @@ def setUp(self): "OUTSTANDING_DCT": "19.00", "ACTUAL_CASH_TRANSFER_DC": "13.00", "OUTSTANDING_DCT_DC": "14.00", - "MULTI_CURR_FLAG": "N" + "MULTI_CURR_FLAG": "N", + "COMPLETED_FLAG": None, } self.expected_headers = { "vendor_code": "Code123", @@ -68,7 +69,8 @@ def setUp(self): "actual_amt_local": "13.00", "outstanding_amt": "19.00", "outstanding_amt_local": "14.00", - "multi_curr_flag": False + "multi_curr_flag": False, + "completed_flag": False, } self.expected_line_item = { "line_item": "987", @@ -110,10 +112,10 @@ def setUp(self): start_date=datetime.date(2015, 1, 13), end_date=datetime.date(2015, 12, 20), ) - self.adapter = synchronizers.FundReservationsSynchronizer(self.country) + self.adapter = synchronizers.FundReservationsSynchronizer(self.country.business_area_code) def test_init(self): - a = synchronizers.FundReservationsSynchronizer(self.country) + a = synchronizers.FundReservationsSynchronizer(self.country.business_area_code) self.assertEqual(a.header_records, {}) self.assertEqual(a.item_records, {}) self.assertEqual(a.fr_headers, {}) @@ -374,10 +376,10 @@ def setUp(self): exchange_rate=self.data["EXCHANGE_RATE"], responsible_person=self.data["RESP_PERSON"], ) - self.adapter = synchronizers.FundCommitmentSynchronizer(self.country) + self.adapter = synchronizers.FundCommitmentSynchronizer(self.country.business_area_code) def test_init(self): - a = synchronizers.FundCommitmentSynchronizer(self.country) + a = synchronizers.FundCommitmentSynchronizer(self.country.business_area_code) self.assertEqual(a.header_records, {}) self.assertEqual(a.item_records, {}) self.assertEqual(a.fc_headers, {}) diff --git a/src/etools/applications/hact/tasks.py b/src/etools/applications/hact/tasks.py index 7d7ddb802e..7834272544 100644 --- a/src/etools/applications/hact/tasks.py +++ b/src/etools/applications/hact/tasks.py @@ -4,11 +4,11 @@ from django.db import connection, transaction from celery.utils.log import get_task_logger +from unicef_vision.exceptions import VisionException from etools.applications.hact.models import AggregateHact from etools.applications.partners.models import PartnerOrganization from etools.applications.users.models import Country -from etools.applications.vision.exceptions import VisionException from etools.applications.vision.models import VisionSyncLog from etools.config.celery import app @@ -16,14 +16,14 @@ @app.task -def update_hact_for_country(country_name): - country = Country.objects.get(name=country_name) +def update_hact_for_country(business_area_code): + country = Country.objects.get(business_area_code=business_area_code) log = VisionSyncLog( country=country, handler_name='HactSynchronizer' ) connection.set_tenant(country) - logger.info('Set country {}'.format(country_name)) + logger.info('Set country {}'.format(business_area_code)) try: partners = PartnerOrganization.objects.hact_active() for partner in partners: @@ -55,7 +55,7 @@ def update_hact_values(*args, **kwargs): if schema_names: countries = countries.filter(schema_name__in=schema_names.split(',')) for country in countries: - update_hact_for_country.delay(country.name) + update_hact_for_country.delay(country.business_area_code) logger.info('Hact Freeze Task generated all tasks') diff --git a/src/etools/applications/hact/tests/test_tasks.py b/src/etools/applications/hact/tests/test_tasks.py index 59e55be788..8c0df0e9a0 100644 --- a/src/etools/applications/hact/tests/test_tasks.py +++ b/src/etools/applications/hact/tests/test_tasks.py @@ -31,7 +31,7 @@ def test_task_create(self): logs = VisionSyncLog.objects.all() self.assertEqual(logs.count(), 0) PartnerFactory(name="Partner XYZ", reported_cy=20000) - update_hact_for_country(self.tenant.name) + update_hact_for_country(self.tenant.business_area_code) self.assertEqual(logs.count(), 1) log = logs.first() diff --git a/src/etools/applications/management/templates/portal.html b/src/etools/applications/management/templates/portal.html index 2f166afc69..6d765178ff 100644 --- a/src/etools/applications/management/templates/portal.html +++ b/src/etools/applications/management/templates/portal.html @@ -47,11 +47,11 @@

eTools Management Dashboard

Reports
PMP Indicators Users - FAM

+ diff --git a/src/etools/applications/management/urls.py b/src/etools/applications/management/urls.py index a1d0b21234..95b35251a5 100644 --- a/src/etools/applications/management/urls.py +++ b/src/etools/applications/management/urls.py @@ -4,6 +4,7 @@ from etools.applications.management.views.reports import LoadResultStructure from etools.applications.management.views.tasks_endpoints import ( PMPIndicatorsReportView, + SyncAllUsers, SyncDeltaUsers, TestSendEmailAPIView, UpdateAggregateHactValuesAPIView, @@ -23,6 +24,7 @@ url(r'^sync-countries/$', SyncCountries.as_view(), name='sync_frs'), url(r'^api/stats/usercounts/$', ActiveUsers.as_view(), name='stats_user_counts'), url(r'^api/stats/agreements/$', AgreementsStatisticsView.as_view(), name='stats_agreements'), + url(r'^tasks/sync_all_users/$', SyncAllUsers.as_view(), name='tasks_sync_all_users'), url(r'^tasks/sync_delta_users/$', SyncDeltaUsers.as_view(), name='tasks_sync_delta_users'), url(r'^tasks/update_hact_values/$', UpdateHactValuesAPIView.as_view(), name='tasks_update_hact_values'), url(r'^tasks/update_aggregate_hact_values/$', UpdateAggregateHactValuesAPIView.as_view(), diff --git a/src/etools/applications/management/views/tasks_endpoints.py b/src/etools/applications/management/views/tasks_endpoints.py index 7e50b7caab..611b5a541d 100644 --- a/src/etools/applications/management/views/tasks_endpoints.py +++ b/src/etools/applications/management/views/tasks_endpoints.py @@ -11,7 +11,7 @@ from etools.applications.hact.tasks import update_aggregate_hact_values, update_hact_values from etools.applications.management.tasks import pmp_indicator_report, send_test_email, user_report -from etools.libraries.azure_graph_api.tasks import sync_delta_users +from etools.libraries.azure_graph_api.tasks import sync_all_users, sync_delta_users class BasicTaskAPIView(APIView, metaclass=ABCMeta): @@ -54,6 +54,11 @@ def get_filename(self): return f'{self.base_filename}_as_of_{today}' +class SyncAllUsers(BasicTaskAPIView): + task_function = sync_all_users + success_message = 'Task generated Successfully: Sync All Users' + + class SyncDeltaUsers(BasicTaskAPIView): task_function = sync_delta_users success_message = 'Task generated Successfully: Sync Delta Users' diff --git a/src/etools/applications/partners/migrations/0036_auto_20190418_1832.py b/src/etools/applications/partners/migrations/0036_auto_20190418_1832.py index b9c7fac61f..6900773af4 100644 --- a/src/etools/applications/partners/migrations/0036_auto_20190418_1832.py +++ b/src/etools/applications/partners/migrations/0036_auto_20190418_1832.py @@ -3,25 +3,6 @@ from django.db import connection, migrations, ProgrammingError -def rename_many_to_many_key(apps, schema_editor): - - with connection.cursor() as cursor: - try: - cursor.execute('SELECT sector_id FROM partners_intervention_sections') - cursor.execute('ALTER TABLE "partners_intervention_sections" RENAME COLUMN "sector_id" TO "section_id";') - except ProgrammingError: - pass # first statement will fail since is already section_id - - -def undo_many_to_many_key(apps, schema_editor): - with connection.cursor() as cursor: - try: - cursor.execute('SELECT section_id FROM partners_intervention_sections') - cursor.execute('ALTER TABLE "partners_intervention_sections" RENAME COLUMN "section_id" TO "sector_id";') - except ProgrammingError: - pass # first statement will fail since is already sector_id - - class Migration(migrations.Migration): dependencies = [ @@ -29,5 +10,4 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(rename_many_to_many_key, undo_many_to_many_key), ] diff --git a/src/etools/applications/partners/migrations/0037_auto_20190502_1407.py b/src/etools/applications/partners/migrations/0037_auto_20190502_1407.py new file mode 100644 index 0000000000..47b5ddbae0 --- /dev/null +++ b/src/etools/applications/partners/migrations/0037_auto_20190502_1407.py @@ -0,0 +1,47 @@ +# Generated by Django 2.1.8 on 2019-05-02 14:07 + +from django.db import migrations + + +def remove_pd_pca_document_types(apps, schema_editor): + """Remove PD/PCA document types, but need to convert those + objects using them. + + 'PD' document types converted to 'Signed PD/SSFA' + 'PCA' document types converted to 'Signed PCA' + """ + AttachmentFileType = apps.get_model('unicef_attachments', 'filetype') + Attachment = apps.get_model('unicef_attachments', 'attachment') + + mapping = [ + ("pd", "signed_pd/ssfa"), + ("pca", "attached_agreement"), + ] + for from_filetype_name, to_filetype_name in mapping: + try: + from_filetype = AttachmentFileType.objects.filter( + name=from_filetype_name + ) + to_filetype = AttachmentFileType.objects.get(name=to_filetype_name) + for attachment in Attachment.objects.filter(file_type__in=from_filetype): + attachment.filetype = to_filetype + attachment.save() + for filetype in from_filetype: + filetype.delete() + except AttachmentFileType.DoesNotExist: + # schema may not have this file type so ignore + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0036_auto_20190418_1832'), + ] + + operations = [ + migrations.RunPython( + remove_pd_pca_document_types, + migrations.RunPython.noop, + ) + ] diff --git a/src/etools/applications/partners/mixins.py b/src/etools/applications/partners/mixins.py index 8f6f2a93b5..eec7e7f061 100644 --- a/src/etools/applications/partners/mixins.py +++ b/src/etools/applications/partners/mixins.py @@ -4,7 +4,7 @@ from etools.applications.partners.models import PartnerOrganization -class HiddenPartnerMixin(object): +class HiddenPartnerMixin: def formfield_for_foreignkey(self, db_field, request=None, **kwargs): if db_field.name == 'partner': @@ -15,7 +15,7 @@ def formfield_for_foreignkey(self, db_field, request=None, **kwargs): ) -class CountryUsersAdminMixin(object): +class CountryUsersAdminMixin: staff_only = True diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 74881dc028..11b41bf9b6 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -31,7 +31,7 @@ from etools.applications.t2f.models import Travel, TravelActivity, TravelType from etools.applications.tpm.models import TPMActivity, TPMVisit from etools.applications.users.models import Office -from etools.libraries.djangolib.models import StringConcat +from etools.libraries.djangolib.models import MaxDistinct, StringConcat from etools.libraries.pythonlib.datetime import get_current_year, get_quarter from etools.libraries.pythonlib.encoders import CustomJSONEncoder @@ -138,7 +138,7 @@ def __str__(self): return self.name -class PartnerType(object): +class PartnerType: BILATERAL_MULTILATERAL = 'Bilateral / Multilateral' CIVIL_SOCIETY_ORGANIZATION = 'Civil Society Organization' GOVERNMENT = 'Government' @@ -725,14 +725,13 @@ def spot_checks(self, event_date=None, update_one=False): hact['spot_checks']['completed'][quarter_name] = scq else: audit_spot_check = SpotCheck.objects.filter( - partner=self, status=Engagement.FINAL, - date_of_draft_report_to_unicef__year=datetime.datetime.now().year - ) + partner=self, date_of_draft_report_to_ip__year=datetime.datetime.now().year + ).exclude(status=Engagement.CANCELLED) - asc1 = audit_spot_check.filter(date_of_draft_report_to_unicef__quarter=1).count() - asc2 = audit_spot_check.filter(date_of_draft_report_to_unicef__quarter=2).count() - asc3 = audit_spot_check.filter(date_of_draft_report_to_unicef__quarter=3).count() - asc4 = audit_spot_check.filter(date_of_draft_report_to_unicef__quarter=4).count() + asc1 = audit_spot_check.filter(date_of_draft_report_to_ip__quarter=1).count() + asc2 = audit_spot_check.filter(date_of_draft_report_to_ip__quarter=2).count() + asc3 = audit_spot_check.filter(date_of_draft_report_to_ip__quarter=3).count() + asc4 = audit_spot_check.filter(date_of_draft_report_to_ip__quarter=4).count() hact['spot_checks']['completed']['q1'] = asc1 hact['spot_checks']['completed']['q2'] = asc2 @@ -759,12 +758,12 @@ def audits_completed(self, update_one=False): else: audits = Audit.objects.filter( partner=self, - status=Engagement.FINAL, - date_of_draft_report_to_unicef__year=datetime.datetime.now().year).count() + date_of_draft_report_to_ip__year=datetime.datetime.now().year + ).exclude(status=Engagement.CANCELLED).count() s_audits = SpecialAudit.objects.filter( partner=self, - status=Engagement.FINAL, - date_of_draft_report_to_unicef__year=datetime.datetime.now().year).count() + date_of_draft_report_to_ip__year=datetime.datetime.now().year + ).exclude(status=Engagement.CANCELLED).count() completed_audit = audits + s_audits hact['audits']['completed'] = completed_audit self.hact_values = hact @@ -774,8 +773,10 @@ def hact_support(self): from etools.applications.audit.models import Audit, Engagement hact = self.get_hact_json() - audits = Audit.objects.filter(partner=self, status=Engagement.FINAL, - date_of_draft_report_to_unicef__year=datetime.datetime.today().year) + audits = Audit.objects.filter( + partner=self, status=Engagement.FINAL, + date_of_draft_report_to_ip__year=datetime.datetime.today().year + ).exclude(status=Engagement.CANCELLED) hact['outstanding_findings'] = sum([ audit.pending_unsupported_amount for audit in audits if audit.pending_unsupported_amount]) hact['assurance_coverage'] = self.assurance_coverage @@ -1499,7 +1500,7 @@ def frs_qs(self): donors=StringConcat("frs__fr_items__donor", separator="|", distinct=True), donor_codes=StringConcat("frs__fr_items__donor_code", separator="|", distinct=True), grants=StringConcat("frs__fr_items__grant_number", separator="|", distinct=True), - max_fr_currency=Max("frs__currency", output_field=CharField(), distinct=True), + max_fr_currency=MaxDistinct("frs__currency", output_field=CharField(), distinct=True), multi_curr_flag=Count(Case(When(frs__multi_curr_flag=True, then=1))) ) return qs diff --git a/src/etools/applications/partners/permission_matrix/intervention_permissions.csv b/src/etools/applications/partners/permission_matrix/intervention_permissions.csv index 7d2c0d1b68..7099cf4a3c 100644 --- a/src/etools/applications/partners/permission_matrix/intervention_permissions.csv +++ b/src/etools/applications/partners/permission_matrix/intervention_permissions.csv @@ -66,6 +66,7 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed 3.2.4,number,UNICEF User,,*,Required,TRUE 3.2.5,title,UNICEF User,,*,Required,TRUE 3.2.7,partner_focal_points,UNICEF User,,Draft,Required,FALSE +3.2.7,submission_date,UNICEF User,,Draft,Required,FALSE 3.2.8,offices,UNICEF User,,Draft,Required,FALSE 3.2.9,unicef_focal_points,UNICEF User,,Draft,Required,FALSE 3.3.1,country_programme,UNICEF User,,Draft,Required,FALSE diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index 6800bb5c14..5f224c24ee 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -18,7 +18,7 @@ READ_ONLY_API_GROUP_NAME = 'Read-Only API' -class PMPPermissions(object): +class PMPPermissions: # this property specifies an array of model properties in order to check against the permission matrix. The fields # declared under this property need to be both property on the model and delcared in the permission matrix EXTRA_FIELDS = [] diff --git a/src/etools/applications/partners/synchronizers.py b/src/etools/applications/partners/synchronizers.py index 7dee4193cc..3db756ab2e 100644 --- a/src/etools/applications/partners/synchronizers.py +++ b/src/etools/applications/partners/synchronizers.py @@ -6,19 +6,18 @@ from django.core.exceptions import ValidationError from django.db import connection, transaction +from unicef_vision.loaders import VISION_NO_DATA_MESSAGE +from unicef_vision.synchronizers import FileDataSynchronizer +from unicef_vision.utils import comp_decimals + from etools.applications.partners.models import PartnerOrganization, PlannedEngagement from etools.applications.partners.tasks import notify_partner_hidden -from etools.applications.vision.utils import comp_decimals -from etools.applications.vision.vision_data_synchronizer import ( - FileDataSynchronizer, - VISION_NO_DATA_MESSAGE, - VisionDataSynchronizer, -) +from etools.applications.vision.synchronizers import VisionDataTenantSynchronizer logger = logging.getLogger(__name__) -class PartnerSynchronizer(VisionDataSynchronizer): +class PartnerSynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetPartnerDetailsInfo_json' REQUIRED_KEYS = ( @@ -282,11 +281,11 @@ class FilePartnerSynchronizer(FileDataSynchronizer, PartnerSynchronizer): >>> from etools.applications.users.models import Country >>> country = Country.objects.get(name='Indonesia') >>> filename = '/home/user/Downloads/partners.json' - >>> FilePartnerSynchronizer(country, filename).sync() + >>> FilePartnerSynchronizer(country.business_area_code, filename).sync() """ -class DirectCashTransferSynchronizer(VisionDataSynchronizer): +class DirectCashTransferSynchronizer(VisionDataTenantSynchronizer): model = PartnerOrganization diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index fdde3237e7..65f7058ad7 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -1762,7 +1762,6 @@ def test_create_amendment_with_internal_prc_review(self): assert flat.pd_ssfa_number def test_create_amendment_with_internal_prc_review_none(self): - self.data["internal_prc_review"] = None response = self._make_request( user=self.partnership_manager_user, data=self.data, @@ -1789,7 +1788,7 @@ def test_create_amendment_when_already_in_amendment(self): ['Cannot add a new amendment while another amendment is in progress.'] ) - def _make_request(self, user=None, data=None, request_format='json', **kwargs): + def _make_request(self, user=None, data="", request_format='json', **kwargs): return self.forced_auth_req('post', self.url, user=user, data=data, request_format=request_format, **kwargs) diff --git a/src/etools/applications/partners/tests/test_models.py b/src/etools/applications/partners/tests/test_models.py index 3072d1b1b8..c7d41a6ee6 100644 --- a/src/etools/applications/partners/tests/test_models.py +++ b/src/etools/applications/partners/tests/test_models.py @@ -535,7 +535,17 @@ def test_spot_checks_update_travel_activity(self): SpotCheckFactory( partner=self.partner_organization, status=Engagement.FINAL, - date_of_draft_report_to_unicef=datetime.datetime(datetime.datetime.today().year, 4, 1) + date_of_draft_report_to_ip=datetime.datetime(datetime.datetime.today().year, 4, 1) + ) + SpotCheckFactory( + partner=self.partner_organization, + status=Engagement.CANCELLED, + date_of_draft_report_to_ip=datetime.datetime(datetime.datetime.today().year, 4, 10) + ) + SpotCheckFactory( + partner=self.partner_organization, + status=Engagement.REPORT_SUBMITTED, + date_of_draft_report_to_ip=None ) self.partner_organization.spot_checks() self.assertEqual(self.partner_organization.hact_values['spot_checks']['completed']['total'], 1) @@ -557,12 +567,22 @@ def test_audits_completed_update_travel_activity(self): AuditFactory( partner=self.partner_organization, status=Engagement.FINAL, - date_of_draft_report_to_unicef=datetime.datetime(datetime.datetime.today().year, 4, 1) + date_of_draft_report_to_ip=datetime.datetime(datetime.datetime.today().year, 4, 1) ) SpecialAuditFactory( + partner=self.partner_organization, + status=Engagement.REPORT_SUBMITTED, + date_of_draft_report_to_ip=datetime.datetime(datetime.datetime.today().year, 8, 1) + ) + AuditFactory( partner=self.partner_organization, status=Engagement.FINAL, - date_of_draft_report_to_unicef=datetime.datetime(datetime.datetime.today().year, 8, 1) + date_of_draft_report_to_ip=None + ) + SpecialAuditFactory( + partner=self.partner_organization, + status=Engagement.CANCELLED, + date_of_draft_report_to_ip=datetime.datetime(datetime.datetime.today().year, 8, 1) ) self.partner_organization.audits_completed() self.assertEqual(self.partner_organization.hact_values['audits']['completed'], 2) diff --git a/src/etools/applications/partners/tests/test_serializers.py b/src/etools/applications/partners/tests/test_serializers.py index 09c41999fd..7feb2edf60 100644 --- a/src/etools/applications/partners/tests/test_serializers.py +++ b/src/etools/applications/partners/tests/test_serializers.py @@ -35,7 +35,7 @@ def setUpTestData(cls): # The serializer examines context['request'].user during the course of its operation. If that's not set, the # serializer will fail. It doesn't need a real request object, just something with a .user attribute, so # that's what I create here. - class Stub(object): + class Stub: pass cls.fake_request = Stub() cls.fake_request.user = cls.user diff --git a/src/etools/applications/partners/tests/test_synchronizers.py b/src/etools/applications/partners/tests/test_synchronizers.py index 1812f51c47..baa1ee8e2e 100644 --- a/src/etools/applications/partners/tests/test_synchronizers.py +++ b/src/etools/applications/partners/tests/test_synchronizers.py @@ -2,6 +2,8 @@ import datetime import json +from unicef_vision.loaders import VISION_NO_DATA_MESSAGE + from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.partners import synchronizers from etools.applications.partners.models import PartnerOrganization @@ -27,7 +29,7 @@ def setUp(self): "TOTAL_CASH_TRANSFERRED_YTD": "70.00", } self.records = {"ROWSET": {"ROW": [self.data]}} - self.adapter = synchronizers.PartnerSynchronizer(self.country) + self.adapter = synchronizers.PartnerSynchronizer(self.country.business_area_code) def test_convert_records(self): self.assertEqual( @@ -51,7 +53,7 @@ def test_get_json(self): self.assertEqual(response, self.data) def test_get_json_no_data(self): - response = self.adapter._get_json(synchronizers.VISION_NO_DATA_MESSAGE) + response = self.adapter._get_json(VISION_NO_DATA_MESSAGE) self.assertEqual(response, []) def test_get_cso_type_none(self): @@ -229,7 +231,7 @@ def setUpTestData(cls): name="New", vendor_number=cls.vendor_key ) - cls.synchronizer = synchronizers.DirectCashTransferSynchronizer(cls.country) + cls.synchronizer = synchronizers.DirectCashTransferSynchronizer(cls.country.business_area_code) def test_create_dict(self): dcts = self.synchronizer.create_dict(self.api_response) diff --git a/src/etools/applications/partners/tests/test_validation_interventions.py b/src/etools/applications/partners/tests/test_validation_interventions.py index fbaecd7a16..72b66c4ac4 100644 --- a/src/etools/applications/partners/tests/test_validation_interventions.py +++ b/src/etools/applications/partners/tests/test_validation_interventions.py @@ -71,6 +71,7 @@ def setUp(self): 'total_actual_amt_usd': 0, 'earliest_start_date': None, 'latest_end_date': None, + 'total_completed_flag': False, } def assertFundamentals(self, data): @@ -88,6 +89,7 @@ def test_end_after_today(self): "End date is in the future" ): transition_to_closed(intervention) + self.expected["total_completed_flag"] = True self.assertFundamentals(intervention.total_frs) def test_total_amounts_outstanding_not_zero(self): @@ -95,7 +97,7 @@ def test_total_amounts_outstanding_not_zero(self): frs = FundsReservationHeaderFactory( intervention=self.intervention, total_amt=0.00, - total_amt_local=0.00, + total_amt_local=10.00, intervention_amt=10.00, actual_amt=0.00, actual_amt_local=10.00, @@ -110,6 +112,7 @@ def test_total_amounts_outstanding_not_zero(self): transition_to_closed(self.intervention) self.expected["total_outstanding_amt"] = 5.00 self.expected["total_intervention_amt"] = 10.00 + self.expected["total_frs_amt"] = 10.00 self.expected["total_actual_amt"] = 10.00 self.expected["earliest_start_date"] = frs.start_date self.expected["latest_end_date"] = frs.end_date @@ -120,12 +123,12 @@ def test_total_amounts_not_equal(self): frs = FundsReservationHeaderFactory( intervention=self.intervention, total_amt=0.00, - total_amt_local=0.00, + total_amt_local=10.00, intervention_amt=10.00, actual_amt_local=20.00, actual_amt=0.00, outstanding_amt_local=0.00, - outstanding_amt=0.00 + outstanding_amt=0.00, ) with self.assertRaisesRegexp( TransitionError, @@ -133,6 +136,7 @@ def test_total_amounts_not_equal(self): 'Total Outstanding DCTs need to equal to 0' ): transition_to_closed(self.intervention) + self.expected["total_frs_amt"] = 10.00 self.expected["total_intervention_amt"] = 10.00 self.expected["total_actual_amt"] = 20.00 self.expected["earliest_start_date"] = frs.start_date diff --git a/src/etools/applications/partners/validation/interventions.py b/src/etools/applications/partners/validation/interventions.py index d874f77a6e..fda53d784c 100644 --- a/src/etools/applications/partners/validation/interventions.py +++ b/src/etools/applications/partners/validation/interventions.py @@ -37,9 +37,12 @@ def transition_to_closed(i): 'total_actual_amt': 0, 'total_actual_amt_usd': 0, 'earliest_start_date': None, - 'latest_end_date': None + 'latest_end_date': None, + 'total_completed_flag': True } for fr in i.frs.filter(): + # either they're all marked as completed or it's False + r['total_completed_flag'] = fr.completed_flag and r['total_completed_flag'] r['total_frs_amt'] += fr.total_amt_local r['total_frs_amt_usd'] += fr.total_amt r['total_outstanding_amt'] += fr.outstanding_amt_local @@ -63,8 +66,13 @@ def transition_to_closed(i): if i.end > today: raise TransitionError([_('End date is in the future')]) - if i.total_frs['total_frs_amt'] != i.total_frs['total_actual_amt'] or \ - i.total_frs['total_outstanding_amt'] != 0: + # In case FRs are marked as completed validation needs to move forward regardless of value discrepancy + # In case it's a supply only PD, the total FRs will be $0.01 and validation needs to move forward + # to be safe given decimal field here compare to a float we're saying smaller than $0.1 & continue with validation + if not i.total_frs['total_completed_flag'] and \ + not float(i.total_frs['total_frs_amt']) < 0.1 and \ + (i.total_frs['total_frs_amt'] != i.total_frs['total_actual_amt'] or + i.total_frs['total_outstanding_amt'] != 0): raise TransitionError([_('Total FR amount needs to equal total actual amount, and ' 'Total Outstanding DCTs need to equal to 0')]) diff --git a/src/etools/applications/partners/views/agreements_v2.py b/src/etools/applications/partners/views/agreements_v2.py index 1a4e04fb24..e2c3a51969 100644 --- a/src/etools/applications/partners/views/agreements_v2.py +++ b/src/etools/applications/partners/views/agreements_v2.py @@ -52,6 +52,7 @@ class AgreementListAPIView(QueryStringFilterMixin, ExportModelMixin, ValidatorVi filters = ( ('agreement_type', 'agreement_type__in'), + ('cpStructures', 'country_programme__in'), ('status', 'status__in'), ('partner_name', 'partner__name__in'), ('start', 'start__gt'), diff --git a/src/etools/applications/partners/views/dashboards.py b/src/etools/applications/partners/views/dashboards.py index 897d033475..e19a3ae50f 100644 --- a/src/etools/applications/partners/views/dashboards.py +++ b/src/etools/applications/partners/views/dashboards.py @@ -14,6 +14,7 @@ from etools.applications.partners.models import FileType, Intervention, InterventionAttachment from etools.applications.partners.serializers.dashboards import InterventionDashSerializer from etools.applications.t2f.models import Travel, TravelActivity, TravelType +from etools.libraries.djangolib.models import MaxDistinct class InterventionPartnershipDashView(QueryStringFilterMixin, ListCreateAPIView): @@ -73,7 +74,7 @@ def get_queryset(self): Sum("frs__actual_amt"), Sum("frs__actual_amt_local"), Count("frs__currency", distinct=True), - max_fr_currency=Max("frs__currency", output_field=CharField(), distinct=True), + max_fr_currency=MaxDistinct("frs__currency", output_field=CharField(), distinct=True), multi_curr_flag=Count(Case(When(frs__multi_curr_flag=True, then=1))), has_final_partnership_review=Subquery(final_partnership_review_qs), action_points=Subquery(action_points_qs), diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index a8f3f023ee..fe012fec8f 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -634,7 +634,7 @@ def delete(self, request, *args, **kwargs): return super().delete(request, *args, **kwargs) -class InterventionLocation(object): +class InterventionLocation: """Helper: we'll use one of these per row of output in InterventionLocationListAPIView""" def __init__(self, intervention, location, section): self.intervention = intervention diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index a5fe6bd299..ff358e44bc 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -20,6 +20,7 @@ from rest_framework.response import Response from rest_framework_csv import renderers as r from unicef_restlib.views import QueryStringFilterMixin +from unicef_vision.utils import get_data_from_insight from etools.applications.action_points.models import ActionPoint from etools.applications.core.mixins import ExportModelMixin @@ -72,7 +73,6 @@ from etools.applications.partners.synchronizers import PartnerSynchronizer from etools.applications.partners.views.helpers import set_tenant_or_fail from etools.applications.t2f.models import Travel, TravelActivity, TravelType -from etools.applications.vision.utils import get_data_from_insight from etools.libraries.djangolib.models import StringConcat @@ -464,7 +464,7 @@ def create(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST) country = request.user.profile.country - partner_sync = PartnerSynchronizer(country=country) + partner_sync = PartnerSynchronizer(business_area_code=country.business_area_code) partner_sync._partner_save(partner_resp, full_sync=False) partner = PartnerOrganization.objects.get( diff --git a/src/etools/applications/partners/views/v1.py b/src/etools/applications/partners/views/v1.py index bd70801100..66ba45947f 100644 --- a/src/etools/applications/partners/views/v1.py +++ b/src/etools/applications/partners/views/v1.py @@ -8,10 +8,10 @@ from easy_pdf.views import PDFTemplateView from rest_framework import mixins, viewsets +from unicef_vision.utils import get_data_from_insight from etools.applications.partners.models import Agreement, FileType from etools.applications.partners.serializers.v1 import FileTypeSerializer -from etools.applications.vision.utils import get_data_from_insight class PCAPDFView(LoginRequiredMixin, PDFTemplateView): diff --git a/src/etools/applications/permissions2/conditions.py b/src/etools/applications/permissions2/conditions.py index 52d3751f2d..1dc236112e 100644 --- a/src/etools/applications/permissions2/conditions.py +++ b/src/etools/applications/permissions2/conditions.py @@ -1,7 +1,7 @@ from etools.applications.permissions2.utils import collect_parent_models, get_model_target -class BaseCondition(object): +class BaseCondition: def to_internal_value(self): raise NotImplementedError diff --git a/src/etools/applications/permissions2/metadata.py b/src/etools/applications/permissions2/metadata.py index 652ecb97fa..a8bcf50615 100644 --- a/src/etools/applications/permissions2/metadata.py +++ b/src/etools/applications/permissions2/metadata.py @@ -8,7 +8,7 @@ ) -class PermissionsBasedMetadataMixin(object): +class PermissionsBasedMetadataMixin: """ Filter fields which user has no read permission to. """ diff --git a/src/etools/applications/permissions2/serializers.py b/src/etools/applications/permissions2/serializers.py index 4cd1cf6c55..d22a47cf36 100644 --- a/src/etools/applications/permissions2/serializers.py +++ b/src/etools/applications/permissions2/serializers.py @@ -6,7 +6,7 @@ from etools.applications.permissions2.models import Permission -class PermissionsBasedSerializerMixin(object): +class PermissionsBasedSerializerMixin: def _collect_permissions_targets(self): """ Collect permissions targets based on serializer's model and field name from full serializers tree. diff --git a/src/etools/applications/permissions2/views.py b/src/etools/applications/permissions2/views.py index 1869f90bb1..a650d882d6 100644 --- a/src/etools/applications/permissions2/views.py +++ b/src/etools/applications/permissions2/views.py @@ -9,7 +9,7 @@ from etools.applications.permissions2.conditions import GroupCondition, NewObjectCondition -class FSMTransitionActionMixin(object): +class FSMTransitionActionMixin: def get_transition(self, action, instance=None): if not instance: instance = self.get_object() @@ -73,7 +73,7 @@ def transition(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) -class PermissionContextMixin(object): +class PermissionContextMixin: def get_object(self): lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field if lookup_url_kwarg not in self.kwargs: diff --git a/src/etools/applications/publics/admin.py b/src/etools/applications/publics/admin.py index 4ebe97aee4..5616c61ae5 100644 --- a/src/etools/applications/publics/admin.py +++ b/src/etools/applications/publics/admin.py @@ -1,35 +1,7 @@ from django.contrib import admin -from django.db.models import ForeignKey, ManyToManyField, OneToOneField from etools.applications.publics import models -from etools.libraries.djangolib.models import EPOCH_ZERO - - -class AdminListMixin(object): - exclude_fields = ['id', 'deleted_at'] - custom_fields = ['is_deleted'] - - def __init__(self, model, admin_site): - """ - Gather all the fields from model meta expect fields in 'exclude_fields'. - Add all fields listed in 'custom_fields'. Those can be admin method, model methods etc. - """ - self.list_display = [field.name for field in model._meta.fields if field.name not in self.exclude_fields] - self.list_display += self.custom_fields - self.search_fields = [field.name for field in model._meta.fields if isinstance( - not field, (OneToOneField, ForeignKey, ManyToManyField)) and field.name not in self.exclude_fields] - - super().__init__(model, admin_site) - - def is_deleted(self, obj): - """ - Display a deleted indicator on the admin page (green/red tick). - """ - if hasattr(obj, "deleted_at"): - return obj.deleted_at == EPOCH_ZERO - - is_deleted.short_description = 'Deleted' - is_deleted.boolean = True +from etools.libraries.djangolib.admin import AdminListMixin class DSARateAdmin(admin.ModelAdmin): @@ -113,7 +85,6 @@ class DSARegionAdmin(AdminListMixin, admin.ModelAdmin): admin.site.register(models.DSARate, DSARateAdmin) -admin.site.register(models.DSARateUpload, DSARateUploadAdmin) admin.site.register(models.TravelAgent, TravelAgentAdmin) admin.site.register(models.TravelExpenseType, TravelExpenseTypeAdmin) admin.site.register(models.Currency, CurrencyAdmin) diff --git a/src/etools/applications/publics/migrations/0005_delete_dsarateupload.py b/src/etools/applications/publics/migrations/0005_delete_dsarateupload.py new file mode 100644 index 0000000000..3bc20eaffd --- /dev/null +++ b/src/etools/applications/publics/migrations/0005_delete_dsarateupload.py @@ -0,0 +1,16 @@ +# Generated by Django 2.1.8 on 2019-04-25 14:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('publics', '0004_auto_20190220_2033'), + ] + + operations = [ + migrations.DeleteModel( + name='DSARateUpload', + ), + ] diff --git a/src/etools/applications/publics/models.py b/src/etools/applications/publics/models.py index 53a2fe6a1c..f6eec427d2 100644 --- a/src/etools/applications/publics/models.py +++ b/src/etools/applications/publics/models.py @@ -1,6 +1,5 @@ from datetime import date, timedelta -from django.contrib.postgres.fields import JSONField from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import QuerySet @@ -247,37 +246,3 @@ def __str__(self): return '{} ({} - {})'.format(self.region.label, self.effective_from_date.isoformat(), self.effective_to_date.isoformat()) - - -class DSARateUpload(models.Model): - UPLOADED = 'uploaded' - PROCESSING = 'processing' - FAILED = 'failed' - DONE = 'done' - STATUS = ( - (UPLOADED, 'Uploaded'), - (PROCESSING, 'Processing'), - (FAILED, 'Failed'), - (DONE, 'Done'), - ) - - dsa_file = models.FileField(upload_to="publics/dsa_rate/", verbose_name=_('DSA File')) - status = models.CharField( - max_length=64, - blank=True, - default='', - choices=STATUS, - verbose_name=_('Status') - ) - upload_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Upload Date')) - errors = JSONField(blank=True, null=True, default=dict, verbose_name=_('Errors')) - - def save(self, *args, **kwargs): - if not self.pk: - self.status = DSARateUpload.UPLOADED - super().save(*args, **kwargs) - # resolve circular imports with inline importing - from etools.applications.publics.tasks import upload_dsa_rates - upload_dsa_rates.delay(self.pk) - else: - super().save(*args, **kwargs) diff --git a/src/etools/applications/publics/synchronizers.py b/src/etools/applications/publics/synchronizers.py index 3b63918eba..c4dd905b58 100644 --- a/src/etools/applications/publics/synchronizers.py +++ b/src/etools/applications/publics/synchronizers.py @@ -5,12 +5,12 @@ from django.core.exceptions import ObjectDoesNotExist from etools.applications.publics.models import Country, Currency, ExchangeRate, TravelAgent, TravelExpenseType -from etools.applications.vision.vision_data_synchronizer import VisionDataSynchronizer +from etools.applications.vision.synchronizers import VisionDataTenantSynchronizer -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) -class CurrencySynchronizer(VisionDataSynchronizer): +class CurrencySynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetCurrencyXrate_JSON' GLOBAL_CALL = True @@ -43,19 +43,19 @@ def _save_records(self, records): code=currency_code, defaults={'name': currency_name, 'decimal_places': decimal_places} ) - log.info('Currency %s was updated.', currency_name) + logger.info('Currency %s was updated.', currency_name) ExchangeRate.objects.update_or_create( currency=currency, defaults={'x_rate': x_rate, 'valid_from': valid_from, 'valid_to': valid_to} ) - log.info('Exchange rate %s was updated.', currency_name) + logger.info('Exchange rate %s was updated.', currency_name) processed += 1 return processed -class TravelAgenciesSynchronizer(VisionDataSynchronizer): +class TravelAgenciesSynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetTravelAgenciesInfo_JSON' GLOBAL_CALL = True @@ -82,10 +82,10 @@ def _save_records(self, records): try: travel_agent = TravelAgent.objects.get(code=vendor_code) - log.debug('Travel agent found with code %s', vendor_code) + logger.debug('Travel agent found with code %s', vendor_code) except ObjectDoesNotExist: travel_agent = TravelAgent(code=vendor_code) - log.debug('Travel agent created with code %s', vendor_code) + logger.debug('Travel agent created with code %s', vendor_code) travel_expense_type, _ = TravelExpenseType.objects.get_or_create(vendor_number=vendor_code, defaults={'title': name}) @@ -106,12 +106,12 @@ def _save_records(self, records): try: country = Country.objects.get(vision_code=country_code) except ObjectDoesNotExist: - log.error('Country not found with vision code %s', country_code) + logger.error('Country not found with vision code %s', country_code) continue travel_agent.country = country travel_agent.save() - log.info('Travel agent %s saved.', travel_agent.name) + logger.info('Travel agent %s saved.', travel_agent.name) return processed diff --git a/src/etools/applications/publics/tasks.py b/src/etools/applications/publics/tasks.py index b093fa5003..3e6cdfbdd4 100644 --- a/src/etools/applications/publics/tasks.py +++ b/src/etools/applications/publics/tasks.py @@ -106,7 +106,7 @@ def import_exchange_rates(xml_structure): logger.info('Exchange rate %s was updated.', currency_name) -class DSARateUploader(object): +class DSARateUploader: FIELDS = ( 'Country Code', 'Country Name', diff --git a/src/etools/applications/publics/tests/test_synchronizers.py b/src/etools/applications/publics/tests/test_synchronizers.py index 4bb9d45b69..ef706193f2 100644 --- a/src/etools/applications/publics/tests/test_synchronizers.py +++ b/src/etools/applications/publics/tests/test_synchronizers.py @@ -28,7 +28,7 @@ def setUp(self): "VALID_FROM": "1-Jan-16", "VALID_TO": "31-Dec-17", } - self.adapter = synchronizers.CurrencySynchronizer(self.country) + self.adapter = synchronizers.CurrencySynchronizer(self.country.business_area_code) def test_convert_records(self): self.assertEqual( @@ -58,7 +58,7 @@ def setUp(self): "VENDOR_CITY": "New York", "VENDOR_CTRY_CODE": "USD", } - self.adapter = synchronizers.TravelAgenciesSynchronizer(self.country) + self.adapter = synchronizers.TravelAgenciesSynchronizer(self.country.business_area_code) def test_convert_records(self): self.assertEqual( diff --git a/src/etools/applications/publics/views.py b/src/etools/applications/publics/views.py index 0a9229a0e3..7aa476f997 100644 --- a/src/etools/applications/publics/views.py +++ b/src/etools/applications/publics/views.py @@ -27,7 +27,7 @@ from etools.applications.t2f.models import ModeOfTravel, TravelType -class GhostDataMixin(object): +class GhostDataMixin: def missing(self, request): parameter_serializer = GhostDataPKSerializer(data=request.GET) parameter_serializer.is_valid(raise_exception=True) diff --git a/src/etools/applications/reports/migrations/0017_auto_20190424_1509.py b/src/etools/applications/reports/migrations/0017_auto_20190424_1509.py new file mode 100644 index 0000000000..b7a6c8f15e --- /dev/null +++ b/src/etools/applications/reports/migrations/0017_auto_20190424_1509.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.8 on 2019-04-24 15:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0016_auto_20190412_1612'), + ] + + operations = [ + migrations.AlterField( + model_name='result', + name='level', + field=models.PositiveIntegerField(editable=False), + ), + migrations.AlterField( + model_name='result', + name='lft', + field=models.PositiveIntegerField(editable=False), + ), + migrations.AlterField( + model_name='result', + name='rght', + field=models.PositiveIntegerField(editable=False), + ), + ] diff --git a/src/etools/applications/reports/synchronizers.py b/src/etools/applications/reports/synchronizers.py index 324e2fd242..b9a1e1a7e6 100644 --- a/src/etools/applications/reports/synchronizers.py +++ b/src/etools/applications/reports/synchronizers.py @@ -5,14 +5,16 @@ from django.db import transaction +from unicef_vision.loaders import VISION_NO_DATA_MESSAGE +from unicef_vision.utils import wcf_json_date_as_date + from etools.applications.reports.models import CountryProgramme, Indicator, Result, ResultType -from etools.applications.vision.utils import wcf_json_date_as_date -from etools.applications.vision.vision_data_synchronizer import VISION_NO_DATA_MESSAGE, VisionDataSynchronizer +from etools.applications.vision.synchronizers import VisionDataTenantSynchronizer logger = logging.getLogger(__name__) -class ResultStructureSynchronizer(object): +class ResultStructureSynchronizer: def __init__(self, data): self.data = data self.cps = {} @@ -178,7 +180,7 @@ def update(self): } -class ProgrammeSynchronizer(VisionDataSynchronizer): +class ProgrammeSynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetProgrammeStructureList_JSON' REQUIRED_KEYS = ( "COUNTRY_PROGRAMME_NAME", @@ -317,7 +319,7 @@ def _save_records(self, records): return synchronizer.update() -class RAMSynchronizer(VisionDataSynchronizer): +class RAMSynchronizer(VisionDataTenantSynchronizer): ENDPOINT = 'GetRAMInfo_JSON' REQUIRED_KEYS = ( "INDICATOR_DESCRIPTION", diff --git a/src/etools/applications/reports/tests/test_synchronizers.py b/src/etools/applications/reports/tests/test_synchronizers.py index a720057cb7..6df154d07b 100644 --- a/src/etools/applications/reports/tests/test_synchronizers.py +++ b/src/etools/applications/reports/tests/test_synchronizers.py @@ -1,10 +1,15 @@ - import datetime import json +from unicef_vision.loaders import VISION_NO_DATA_MESSAGE + from etools.applications.core.tests.cases import BaseTenantTestCase -from etools.applications.reports import synchronizers from etools.applications.reports.models import CountryProgramme, Indicator, Result, ResultType +from etools.applications.reports.synchronizers import ( + ProgrammeSynchronizer, + RAMSynchronizer, + ResultStructureSynchronizer, +) from etools.applications.reports.tests.factories import ( CountryProgrammeFactory, IndicatorFactory, @@ -23,7 +28,7 @@ def setUpTestData(cls): def setUp(self): self.data = {"test": "123"} - self.adapter = synchronizers.ResultStructureSynchronizer(self.data) + self.adapter = ResultStructureSynchronizer(self.data) def test_init(self): self.assertEqual(self.adapter.data, self.data) @@ -384,13 +389,13 @@ def setUp(self): "PROGRAMME_AREA_CODE": "", "PROGRAMME_AREA_NAME": "", } - self.adapter = synchronizers.ProgrammeSynchronizer(self.country) + self.adapter = ProgrammeSynchronizer(self.country.business_area_code) def test_get_json(self): data = {"test": "123"} self.assertEqual(self.adapter._get_json(data), data) self.assertEqual( - self.adapter._get_json(synchronizers.VISION_NO_DATA_MESSAGE), + self.adapter._get_json(VISION_NO_DATA_MESSAGE), [] ) @@ -591,7 +596,7 @@ def setUp(self): "BASELINE": "BLINE", "TARGET": "Target", } - self.adapter = synchronizers.RAMSynchronizer(self.country) + self.adapter = RAMSynchronizer(self.country.business_area_code) def test_convert_records(self): records = json.dumps([self.data]) diff --git a/src/etools/applications/t2f/__init__.py b/src/etools/applications/t2f/__init__.py index c48020c386..e69de29bb2 100644 --- a/src/etools/applications/t2f/__init__.py +++ b/src/etools/applications/t2f/__init__.py @@ -1,22 +0,0 @@ - -from django.utils.translation import ugettext_lazy - - -class UserTypes(object): - ANYONE = 'Anyone' - TRAVELER = 'Traveler' - TRAVEL_ADMINISTRATOR = 'Travel Administrator' - SUPERVISOR = 'Supervisor' - TRAVEL_FOCAL_POINT = 'Travel Focal Point' - FINANCE_FOCAL_POINT = 'Finance Focal Point' - REPRESENTATIVE = 'Representative' - - CHOICES = ( - (ANYONE, ugettext_lazy('Anyone')), - (TRAVELER, ugettext_lazy('Traveler')), - (TRAVEL_ADMINISTRATOR, ugettext_lazy('Travel Administrator')), - (SUPERVISOR, ugettext_lazy('Supervisor')), - (TRAVEL_FOCAL_POINT, ugettext_lazy('Travel Focal Point')), - (FINANCE_FOCAL_POINT, ugettext_lazy('Finance Focal Point')), - (REPRESENTATIVE, ugettext_lazy('Representative')), - ) diff --git a/src/etools/applications/t2f/admin.py b/src/etools/applications/t2f/admin.py index 251c0f7f45..c1f177c737 100644 --- a/src/etools/applications/t2f/admin.py +++ b/src/etools/applications/t2f/admin.py @@ -1,10 +1,10 @@ from django.contrib import admin from etools.applications.action_points.admin import ActionPointAdmin -from etools.applications.publics.admin import AdminListMixin from etools.applications.t2f import models from etools.applications.t2f.forms import T2FActionPointAdminForm from etools.applications.t2f.models import T2FActionPoint +from etools.libraries.djangolib.admin import AdminListMixin @admin.register(models.Travel) diff --git a/src/etools/applications/t2f/helpers/clone_travel.py b/src/etools/applications/t2f/helpers/clone_travel.py index 2b63e23064..f56280d7b3 100644 --- a/src/etools/applications/t2f/helpers/clone_travel.py +++ b/src/etools/applications/t2f/helpers/clone_travel.py @@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist -class CloneTravelHelper(object): +class CloneTravelHelper: def __init__(self, travel): self.travel = travel diff --git a/src/etools/applications/t2f/helpers/permission_matrix.py b/src/etools/applications/t2f/helpers/permission_matrix.py index e7f9896646..0fd911206c 100644 --- a/src/etools/applications/t2f/helpers/permission_matrix.py +++ b/src/etools/applications/t2f/helpers/permission_matrix.py @@ -1,15 +1,100 @@ -import json -import os from collections import defaultdict from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ -from etools.applications import t2f -from etools.applications.t2f import UserTypes +from etools.applications.t2f.permission_action_points import action_points from etools.applications.t2f.permissions import permissions PERMISSION_MATRIX_CACHE_KEY = 't2f_permission_matrix' +REPORT_PERMISSIONS = { + 'Supervisor': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': False, 'view': True}, + 'planned': {'edit': False, 'view': True}, + 'submitted': {'edit': False, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': False, 'view': True} + }, + 'Finance Focal Point': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': False, 'view': True}, + 'planned': {'edit': False, 'view': True}, + 'submitted': {'edit': False, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': False, 'view': True} + }, + 'God': { + 'completed': {'edit': True, 'view': True}, + 'rejected': {'edit': True, 'view': True}, + 'planned': {'edit': True, 'view': True}, + 'submitted': {'edit': True, 'view': True}, + 'cancelled': {'edit': True, 'view': True}, + 'approved': {'edit': True, 'view': True} + }, + 'Anyone': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': False, 'view': True}, + 'planned': {'edit': False, 'view': True}, + 'submitted': {'edit': False, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': False, 'view': True} + }, + 'Representative': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': False, 'view': True}, + 'planned': {'edit': False, 'view': True}, + 'submitted': {'edit': False, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': False, 'view': True} + }, + 'Travel Focal Point': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': False, 'view': True}, + 'planned': {'edit': False, 'view': True}, + 'submitted': {'edit': False, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': False, 'view': True} + }, + 'Traveler': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': True, 'view': True}, + 'planned': {'edit': True, 'view': True}, + 'submitted': {'edit': True, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': True, 'view': True} + }, + 'Travel Administrator': { + 'completed': {'edit': False, 'view': True}, + 'rejected': {'edit': True, 'view': True}, + 'planned': {'edit': True, 'view': True}, + 'submitted': {'edit': True, 'view': True}, + 'cancelled': {'edit': False, 'view': True}, + 'approved': {'edit': False, 'view': True} + } +} + + +class UserTypes: + ANYONE = 'Anyone' + TRAVELER = 'Traveler' + TRAVEL_ADMINISTRATOR = 'Travel Administrator' + SUPERVISOR = 'Supervisor' + TRAVEL_FOCAL_POINT = 'Travel Focal Point' + FINANCE_FOCAL_POINT = 'Finance Focal Point' + REPRESENTATIVE = 'Representative' + + CHOICES = ( + (ANYONE, _('Anyone')), + (TRAVELER, _('Traveler')), + (TRAVEL_ADMINISTRATOR, _('Travel Administrator')), + (SUPERVISOR, _('Supervisor')), + (TRAVEL_FOCAL_POINT, _('Travel Focal Point')), + (FINANCE_FOCAL_POINT, _('Finance Focal Point')), + (REPRESENTATIVE, _('Representative')), + ) + def get_user_role_list(user, travel=None): roles = [UserTypes.ANYONE] @@ -36,22 +121,52 @@ def get_user_role_list(user, travel=None): return roles +def convert_permissions_structure(): + """Convert permissions from + (edit/view, model, field) | permission + to + model | field | edit/view | permission + """ + to_format = {} + for user, states in permissions.items(): + to_format[user] = {} + for state, actions in states.items(): + data = defaultdict(dict) + for key, perm in actions.items(): + action, model, field = key + model = "baseDetails" if model == "travel" else model + if model not in data: + data[model] = { + field: {action: perm}, + action: perm, + "edit" if action == "view" else "view": False, + } + else: + if field not in data[model]: + data[model][field] = {action: perm} + else: + data[model][field][action] = perm + if perm: + data[model][action] = perm + to_format[user][state] = data + # add report model permissions + to_format[user][state]["report"] = REPORT_PERMISSIONS[user][state] + return to_format + + def get_permission_matrix(): permission_matrix = cache.get(PERMISSION_MATRIX_CACHE_KEY) if not permission_matrix: - path = os.path.join( - os.path.dirname(t2f.__file__), - "permission_matrix.json", - ) - - with open(path) as permission_matrix_file: - permission_matrix = json.loads(permission_matrix_file.read()) + permission_matrix = { + "action_point": action_points, + "travel": convert_permissions_structure() + } cache.set(PERMISSION_MATRIX_CACHE_KEY, permission_matrix) return permission_matrix -class PermissionMatrix(object): +class PermissionMatrix: VIEW = 'view' EDIT = 'edit' diff --git a/src/etools/applications/t2f/management/commands/et2f_init.py b/src/etools/applications/t2f/management/commands/et2f_init.py index 1c239515ef..44523539c1 100644 --- a/src/etools/applications/t2f/management/commands/et2f_init.py +++ b/src/etools/applications/t2f/management/commands/et2f_init.py @@ -7,17 +7,15 @@ from django.db import connection from django.db.transaction import atomic -from etools.applications.partners.models import PartnerOrganization from etools.applications.publics.models import ( AirlineCompany, BusinessArea, BusinessRegion, Country, Currency, - DSARegion, TravelExpenseType, ) -from etools.applications.users.models import Country as UserCountry, Office +from etools.applications.users.models import Country as UserCountry # DEVELOPMENT CODE - @@ -36,7 +34,6 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('username', nargs=1) parser.add_argument('password', nargs=1, default='password') - parser.add_argument('-u', '--with_users', action='store_true', default=False) parser.add_argument('-o', '--with_offices', action='store_true', default=False) parser.add_argument('-p', '--with_partners', action='store_true', default=False) @@ -50,22 +47,12 @@ def handle(self, *args, **options): country = user.profile.country connection.set_tenant(country) - if options.get('with_users'): - self._load_users() - self._load_airlines() - if options.get('with_offices'): - self._load_offices() - - if options.get('with_partners'): - self._load_partners() - self._load_business_areas() self._load_currencies() self._load_countries() - self._load_dsa_regions(country) self._add_expense_types() self._add_user_groups() @@ -284,26 +271,6 @@ def _load_currencies(self): 'ISK', 'TTD', 'OMR', 'BRL', 'SBD', 'PLN', 'KES', 'SVC', 'USD', 'AZM', 'TOP', 'GNF', 'WST', 'IQD', 'ERN', 'BAM', 'SCR', 'CAD', 'GYD', 'KWD', 'BIF', 'PGK', 'SOS', 'SGD', 'UZS', 'STD', 'IRR', 'CNY', 'XOF', 'TND', 'NZD', 'LVL', 'BSD', 'KGS', 'ARS', 'BMD', 'RSD', 'BHD', 'JPY', 'SDG'] - # data = [('United States dollar', 'USD'), - # ('Euro', 'EUR'), - # ('Japanese yen', 'JPY'), - # ('Pound sterling', 'GBP'), - # ('Australian dollar', 'AUD'), - # ('Canadian dollar', 'CAD'), - # ('Swiss franc', 'CHF'), - # ('Chinese yuan', 'CNY'), - # ('Swedish krona', 'SEK'), - # ('Mexican peso', 'MXN'), - # ('New Zealand dollar', 'NZD'), - # ('Singapore dollar', 'SGD'), - # ('Hong Kong dollar', 'HKD'), - # ('Norwegian krone', 'NOK'), - # ('South Korean won', 'KRW'), - # ('Turkish lira', 'TRY'), - # ('Indian rupee', 'INR'), - # ('Russian ruble', 'RUB'), - # ('Brazilian real', 'BRL'), - # ('South African rand', 'ZAR')] for code in data: name = code @@ -797,46 +764,6 @@ def _load_countries(self): else: self.stdout.write('Country found: {}'.format(name)) - def _load_users(self): - User = get_user_model() - user_full_names = ['Kathryn Cruz', 'Jonathan Wright', 'Timothy Kelly', 'Brenda Nguyen', 'Matthew Morales', - 'Timothy Watson', 'Jacqueline Brooks', 'Steve Olson', 'Lawrence Patterson', 'Lois Jones', - 'Margaret White', 'Clarence Stanley', 'Bruce Williamson', 'Susan Carroll', 'Philip Wood', - 'Emily Jenkins', 'Christina Robinson', 'Jason Young', 'Joyce Freeman', 'Jack Murphy', - 'Katherine Garcia', 'Sean Perkins', 'Howard Peterson', 'Denise Coleman', 'Benjamin Evans', - 'Carl Watkins', 'Martin Morris', 'Nicole Stephens', 'Thomas Willis', 'Ann Ferguson', - 'Russell Hanson', 'Janet Johnston', 'Adam Bowman', 'Elizabeth Mendoza', 'Helen Robertson', - 'Wanda Fowler', 'Roger Richardson', 'Bobby Carroll', 'Donna Sims', 'Shawn Peters', - 'Lisa Davis', 'Laura Riley', 'Jason Freeman', 'Ashley Hill', 'Joseph Gonzales', - 'Brenda Dixon', 'Paul Wilson', 'Tammy Reyes', 'Beverly Bishop', 'James Weaver', - 'Samuel Vasquez', 'Albert Baker', 'Keith Wright', 'Michael Hart', 'Shirley Allen', - 'Samuel Gutierrez', 'Cynthia Riley', 'Roy Simpson', 'Raymond Wagner', 'Eric Taylor', - 'Steven Bell', 'Jane Powell', 'Paula Morales', 'James Hamilton', 'Shirley Perez', - 'Maria Olson', 'Amy Dunn', 'Frances Bowman', 'Billy Lawrence', 'Beverly Howell', 'Amy Sims', - 'Carlos Sanchez', 'Nicholas Harvey', 'Walter Wheeler', 'Bruce Morales', 'Kathy Reynolds', - 'Lisa Lopez', 'Ann Medina', 'Raymond Washington', 'Jessica Brown', 'Harold Stone', - 'Paul Hill', 'Wayne Foster', 'Brian Garza', 'Craig Sims', 'Adam Morales', 'Brandon Miller', - 'Dennis Green', 'Linda Banks', 'Sandra Dunn', 'Randy Rogers', 'Jimmy West', 'Julia Grant', - 'Judy Ryan', 'William Carroll', 'Mary Rose', 'Ann Nelson', 'Rebecca Hill', 'Robert Rivera', - 'Rebecca Weaver'] - - for full_name in user_full_names: - first_name, last_name = full_name.split() - username = full_name.replace(' ', '_').lower() - email = f"{username}@example.com" - u, created = User.objects.get_or_create( - username=username, - email=email, - defaults={ - 'first_name': first_name, - 'last_name': last_name - }, - ) - if created: - self.stdout.write('User created: {} ({})'.format(full_name, username)) - else: - self.stdout.write('User found: {} ({})'.format(full_name, username)) - def _load_airlines(self): airlines = [('American Airlines', 'AA', 1, 'AAL', 'United States'), ('Blue Panorama', 'BV', 4, 'BPA', 'Italy'), @@ -963,69 +890,6 @@ def _load_airlines(self): else: self.stdout.write('Airline found: {}'.format(airline_name)) - def _load_offices(self): - offices = ('Pulilab', 'Unicef HQ') - - for office_name in offices: - o, created = Office.objects.get_or_create(name=office_name) - if created: - o.offices.add(connection.tenant) - self.stdout.write('Office created: {}'.format(office_name)) - else: - self.stdout.write('Office found: {}'.format(office_name)) - - def _load_partners(self): - partners = ['Dynazzy', 'Yodoo', 'Omba', 'Eazzy', 'Avamba', 'Jaxworks', 'Thoughtmix', 'Bubbletube', 'Mydo', - 'Photolist', 'Gevee', 'Buzzdog', 'Quinu', 'Edgewire', 'Yambee', 'Ntag', 'Muxo', - 'Edgetag', 'Tagfeed', 'BlogXS', 'Feedbug', 'Babblestorm', 'Skimia', 'Linkbridge', 'Fatz', 'Kwimbee', - 'Yodo', 'Skibox', 'Zoomzone', 'Meemm', 'Twitterlist', 'Kwilith', 'Skipfire', 'Wikivu', 'Topicblab', - 'BlogXS', 'Brightbean', 'Skimia', 'Mycat', 'Tagcat', 'Meedoo', 'Vitz', 'Realblab', 'Babbleopia', - 'Pixonyx', 'Dabshots', 'Gabcube', 'Yoveo', 'Realblab', 'Tagcat'] - - for partner_name in partners: - p, created = PartnerOrganization.objects.get_or_create(name=partner_name) - if created: - self.stdout.write('Partner created: {}'.format(partner_name)) - else: - self.stdout.write('Partner found: {}'.format(partner_name)) - - def _load_dsa_regions(self, country): - dsa_region_data = [ - { - 'country': Country.objects.filter(name='Hungary').last(), - 'area_name': 'Everywhere', - 'area_code': country.business_area_code, - # 'user_defined': { - # 'dsa_amount_usd': 300, - # 'room_rate': 120, - # 'dsa_amount_60plus_usd': 200, - # 'dsa_amount_60plus_local': 56000, - # 'dsa_amount_local': 84000, - # 'finalization_date': datetime.now().date(), - # 'eff_date': datetime.now().date(), - # } - }, { - 'country': Country.objects.filter(name='Germany').last(), - 'area_name': 'Everywhere', - 'area_code': country.business_area_code, - # 'user_defined': { - # 'dsa_amount_usd': 400, - # 'room_rate': 150, - # 'dsa_amount_60plus_usd': 260, - # 'dsa_amount_60plus_local': 238.68, - # 'dsa_amount_local': 367.21, - # 'finalization_date': datetime.now().date(), - # 'eff_date': datetime.now().date(), - # } - }] - for data in dsa_region_data: - name = data.pop('country') - d, created = DSARegion.objects.get_or_create(country=name, defaults=data) - if created: - self.stdout.write('DSA Region created: {}'.format(name)) - else: - self.stdout.write('DSA Region found: {}'.format(name)) - def _add_expense_types(self): expense_type_data = [ {'title': 'Food', diff --git a/src/etools/applications/t2f/models.py b/src/etools/applications/t2f/models.py index 3a63772e61..ced6b09aca 100644 --- a/src/etools/applications/t2f/models.py +++ b/src/etools/applications/t2f/models.py @@ -21,7 +21,7 @@ from etools.applications.t2f.serializers.mailing import TravelMailSerializer from etools.applications.users.models import WorkspaceCounter -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class TransitionError(RuntimeError): @@ -30,7 +30,7 @@ class TransitionError(RuntimeError): """ -class TravelType(object): +class TravelType: PROGRAMME_MONITORING = 'Programmatic Visit' SPOT_CHECK = 'Spot Check' ADVOCACY = 'Advocacy' @@ -51,7 +51,7 @@ class TravelType(object): # TODO: all of these models that only have 1 field should be a choice field on the models that are using it # for many-to-many array fields are recommended -class ModeOfTravel(object): +class ModeOfTravel: PLANE = 'Plane' BUS = 'Bus' CAR = 'Car' @@ -298,7 +298,7 @@ def mark_as_completed(self): act.partner.spot_checks(event_date=self.end_date, update_one=True) except Exception: - log.exception('Exception while trying to update hact values.') + logger.exception('Exception while trying to update hact values.') @transition(status, target=PLANNED) def reset_status(self): diff --git a/src/etools/applications/t2f/permission_action_points.py b/src/etools/applications/t2f/permission_action_points.py new file mode 100644 index 0000000000..e923180370 --- /dev/null +++ b/src/etools/applications/t2f/permission_action_points.py @@ -0,0 +1,58 @@ +action_points = { + 'Assigner': { + 'action_point_number': {'edit': False, 'view': True}, + 'actions_taken': {'edit': False, 'view': True}, + 'comments': {'edit': False, 'view': True}, + 'completed_at': {'edit': False, 'view': True}, + 'created_at': {'edit': False, 'view': True}, + 'description': {'edit': True, 'view': True}, + 'due_date': {'edit': True, 'view': True}, + 'follow_up': {'edit': True, 'view': True}, + 'id': {'edit': False, 'view': True}, + 'person_responsible': {'edit': True, 'view': True}, + 'status': {'edit': True, 'view': True}, + 'trip_reference_number': {'edit': False, 'view': True} + }, + 'Others': { + 'action_point_number': {'edit': False, 'view': True}, + 'actions_taken': {'edit': False, 'view': True}, + 'comments': {'edit': False, 'view': True}, + 'completed_at': {'edit': False, 'view': True}, + 'created_at': {'edit': False, 'view': True}, + 'description': {'edit': False, 'view': True}, + 'due_date': {'edit': False, 'view': True}, + 'follow_up': {'edit': False, 'view': True}, + 'id': {'edit': False, 'view': True}, + 'person_responsible': {'edit': False, 'view': True}, + 'status': {'edit': False, 'view': True}, + 'trip_reference_number': {'edit': False, 'view': True} + }, + 'PME': { + 'action_point_number': {'edit': False, 'view': True}, + 'actions_taken': {'edit': False, 'view': True}, + 'comments': {'edit': False, 'view': True}, + 'completed_at': {'edit': True, 'view': True}, + 'created_at': {'edit': False, 'view': True}, + 'description': {'edit': False, 'view': True}, + 'due_date': {'edit': True, 'view': True}, + 'follow_up': {'edit': True, 'view': True}, + 'id': {'edit': False, 'view': True}, + 'person_responsible': {'edit': True, 'view': True}, + 'status': {'edit': True, 'view': True}, + 'trip_reference_number': {'edit': False, 'view': True} + }, + 'PersonResponsible': { + 'action_point_number': {'edit': False, 'view': True}, + 'actions_taken': {'edit': True, 'view': True}, + 'comments': {'edit': False, 'view': True}, + 'completed_at': {'edit': True, 'view': True}, + 'created_at': {'edit': False, 'view': True}, + 'description': {'edit': False, 'view': True}, + 'due_date': {'edit': False, 'view': True}, + 'follow_up': {'edit': False, 'view': True}, + 'id': {'edit': False, 'view': True}, + 'person_responsible': {'edit': False, 'view': True}, + 'status': {'edit': True, 'view': True}, + 'trip_reference_number': {'edit': False, 'view': True} + } +} diff --git a/src/etools/applications/t2f/permission_matrix.json b/src/etools/applications/t2f/permission_matrix.json deleted file mode 100644 index 3d85f69abd..0000000000 --- a/src/etools/applications/t2f/permission_matrix.json +++ /dev/null @@ -1 +0,0 @@ -{"action_point": {"PersonResponsible": {"status": {"edit": true, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "completed_at": {"edit": true, "view": true}, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}}, "PME": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "completed_at": {"edit": true, "view": true}, "person_responsible": {"edit": true, "view": true}, "id": {"edit": false, "view": true}}, "Assigner": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "person_responsible": {"edit": true, "view": true}, "id": {"edit": false, "view": true}}, "Others": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}}}, "travel": {"Supervisor": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}, "Finance Focal Point": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}, "God": {"completed": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}}, "Anyone": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": false}, "cost_assignments": {"wbs": {"edit": false, "view": false}, "grant": {"edit": false, "view": false}, "edit": false, "share": {"edit": false, "view": false}, "fund": {"edit": false, "view": false}, "delegate": {"edit": false, "view": false}, "business_area": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": false}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": false}, "edit": false, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": false, "view": false}, "type": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "deductions": {"accomodation": {"edit": false, "view": false}, "edit": false, "no_dsa": {"edit": false, "view": false}, "breakfast": {"edit": false, "view": false}, "lunch": {"edit": false, "view": false}, "dinner": {"edit": false, "view": false}, "date": {"edit": false, "view": false}, "day_of_the_week": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": false}, "cost_assignments": {"wbs": {"edit": false, "view": false}, "grant": {"edit": false, "view": false}, "edit": false, "share": {"edit": false, "view": false}, "fund": {"edit": false, "view": false}, "delegate": {"edit": false, "view": false}, "business_area": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": false}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": false}, "edit": false, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": false, "view": false}, "type": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "deductions": {"accomodation": {"edit": false, "view": false}, "edit": false, "no_dsa": {"edit": false, "view": false}, "breakfast": {"edit": false, "view": false}, "lunch": {"edit": false, "view": false}, "dinner": {"edit": false, "view": false}, "date": {"edit": false, "view": false}, "day_of_the_week": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": false}, "cost_assignments": {"wbs": {"edit": false, "view": false}, "grant": {"edit": false, "view": false}, "edit": false, "share": {"edit": false, "view": false}, "fund": {"edit": false, "view": false}, "delegate": {"edit": false, "view": false}, "business_area": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": false}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": false}, "edit": false, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": false, "view": false}, "type": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "deductions": {"accomodation": {"edit": false, "view": false}, "edit": false, "no_dsa": {"edit": false, "view": false}, "breakfast": {"edit": false, "view": false}, "lunch": {"edit": false, "view": false}, "dinner": {"edit": false, "view": false}, "date": {"edit": false, "view": false}, "day_of_the_week": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": false}, "cost_assignments": {"wbs": {"edit": false, "view": false}, "grant": {"edit": false, "view": false}, "edit": false, "share": {"edit": false, "view": false}, "fund": {"edit": false, "view": false}, "delegate": {"edit": false, "view": false}, "business_area": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": false}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": false}, "edit": false, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": false, "view": false}, "type": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "deductions": {"accomodation": {"edit": false, "view": false}, "edit": false, "no_dsa": {"edit": false, "view": false}, "breakfast": {"edit": false, "view": false}, "lunch": {"edit": false, "view": false}, "dinner": {"edit": false, "view": false}, "date": {"edit": false, "view": false}, "day_of_the_week": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": false}, "cost_assignments": {"wbs": {"edit": false, "view": false}, "grant": {"edit": false, "view": false}, "edit": false, "share": {"edit": false, "view": false}, "fund": {"edit": false, "view": false}, "delegate": {"edit": false, "view": false}, "business_area": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": false}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": false}, "edit": false, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": false, "view": false}, "type": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "deductions": {"accomodation": {"edit": false, "view": false}, "edit": false, "no_dsa": {"edit": false, "view": false}, "breakfast": {"edit": false, "view": false}, "lunch": {"edit": false, "view": false}, "dinner": {"edit": false, "view": false}, "date": {"edit": false, "view": false}, "day_of_the_week": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": false}, "cost_assignments": {"wbs": {"edit": false, "view": false}, "grant": {"edit": false, "view": false}, "edit": false, "share": {"edit": false, "view": false}, "fund": {"edit": false, "view": false}, "delegate": {"edit": false, "view": false}, "business_area": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": false}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": false}, "edit": false, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": false, "view": false}, "type": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "deductions": {"accomodation": {"edit": false, "view": false}, "edit": false, "no_dsa": {"edit": false, "view": false}, "breakfast": {"edit": false, "view": false}, "lunch": {"edit": false, "view": false}, "dinner": {"edit": false, "view": false}, "date": {"edit": false, "view": false}, "day_of_the_week": {"edit": false, "view": false}, "id": {"edit": false, "view": false}, "view": false}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}, "Representative": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": false, "view": true}, "due_date": {"edit": false, "view": true}, "description": {"edit": false, "view": true}, "follow_up": {"edit": false, "view": true}, "trip_reference_number": {"edit": false, "view": true}, "actions_taken": {"edit": false, "view": true}, "created_at": {"edit": false, "view": true}, "completed_at": {"edit": false, "view": true}, "comments": {"edit": false, "view": true}, "action_point_number": {"edit": false, "view": true}, "edit": false, "person_responsible": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}, "Travel Focal Point": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}, "Traveler": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": false, "view": false}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}, "Travel Administrator": {"completed": {"activities": {"primary_traveler": {"edit": false, "view": true}, "edit": false, "partnership": {"edit": false, "view": true}, "locations": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "result": {"edit": false, "view": true}, "travel_type": {"edit": false, "view": true}, "partner": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}, "rejected": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "planned": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "submitted": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "cancelled": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": true, "view": true}, "cost_assignments": {"wbs": {"edit": true, "view": true}, "grant": {"edit": true, "view": true}, "edit": true, "share": {"edit": true, "view": true}, "fund": {"edit": true, "view": true}, "delegate": {"edit": true, "view": true}, "business_area": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "baseDetails": {"status": {"edit": true, "view": true}, "reference_number": {"edit": true, "view": true}, "supervisor": {"edit": true, "view": true}, "end_date": {"edit": true, "view": true}, "office": {"edit": true, "view": true}, "section": {"edit": true, "view": true}, "international_travel": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "ta_required": {"edit": true, "view": true}, "currency": {"edit": true, "view": true}, "purpose": {"edit": true, "view": true}, "traveler": {"edit": true, "view": true}, "start_date": {"edit": true, "view": true}}, "expenses": {"amount": {"edit": true, "view": true}, "edit": true, "account_currency": {"edit": true, "view": true}, "document_currency": {"edit": true, "view": true}, "type": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": true, "view": true}, "edit": true, "no_dsa": {"edit": true, "view": true}, "breakfast": {"edit": true, "view": true}, "lunch": {"edit": true, "view": true}, "dinner": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "day_of_the_week": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "itinerary": {"origin": {"edit": true, "view": true}, "arrival_date": {"edit": true, "view": true}, "mode_of_travel": {"edit": true, "view": true}, "departure_date": {"edit": true, "view": true}, "destination": {"edit": true, "view": true}, "airlines": {"edit": true, "view": true}, "edit": true, "dsa_region": {"edit": true, "view": true}, "view": true, "id": {"edit": true, "view": true}, "overnight_travel": {"edit": true, "view": true}}, "report": {"edit": true, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": true, "view": true}, "security_clearance": {"edit": true, "view": true}, "edit": true, "medical_clearance": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}}, "approved": {"activities": {"primary_traveler": {"edit": true, "view": true}, "edit": true, "partnership": {"edit": true, "view": true}, "locations": {"edit": true, "view": true}, "date": {"edit": true, "view": true}, "result": {"edit": true, "view": true}, "travel_type": {"edit": true, "view": true}, "partner": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "cost_summary": {"edit": false, "view": true}, "cost_assignments": {"wbs": {"edit": false, "view": true}, "grant": {"edit": false, "view": true}, "edit": false, "share": {"edit": false, "view": true}, "fund": {"edit": false, "view": true}, "delegate": {"edit": false, "view": true}, "business_area": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "baseDetails": {"status": {"edit": false, "view": true}, "reference_number": {"edit": false, "view": true}, "supervisor": {"edit": false, "view": true}, "end_date": {"edit": false, "view": true}, "office": {"edit": false, "view": true}, "section": {"edit": false, "view": true}, "international_travel": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "ta_required": {"edit": false, "view": true}, "currency": {"edit": false, "view": true}, "purpose": {"edit": false, "view": true}, "traveler": {"edit": false, "view": true}, "start_date": {"edit": false, "view": true}}, "expenses": {"amount": {"edit": false, "view": true}, "edit": false, "account_currency": {"edit": false, "view": true}, "document_currency": {"edit": false, "view": true}, "type": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "deductions": {"accomodation": {"edit": false, "view": true}, "edit": false, "no_dsa": {"edit": false, "view": true}, "breakfast": {"edit": false, "view": true}, "lunch": {"edit": false, "view": true}, "dinner": {"edit": false, "view": true}, "date": {"edit": false, "view": true}, "day_of_the_week": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}, "itinerary": {"origin": {"edit": false, "view": true}, "arrival_date": {"edit": false, "view": true}, "mode_of_travel": {"edit": false, "view": true}, "departure_date": {"edit": false, "view": true}, "destination": {"edit": false, "view": true}, "airlines": {"edit": false, "view": true}, "edit": false, "dsa_region": {"edit": false, "view": true}, "view": true, "id": {"edit": false, "view": true}, "overnight_travel": {"edit": false, "view": true}}, "report": {"edit": false, "view": true}, "action_points": {"status": {"edit": true, "view": true}, "due_date": {"edit": true, "view": true}, "description": {"edit": true, "view": true}, "follow_up": {"edit": true, "view": true}, "trip_reference_number": {"edit": true, "view": true}, "actions_taken": {"edit": true, "view": true}, "created_at": {"edit": true, "view": true}, "completed_at": {"edit": true, "view": true}, "comments": {"edit": true, "view": true}, "action_point_number": {"edit": true, "view": true}, "edit": true, "person_responsible": {"edit": true, "view": true}, "id": {"edit": true, "view": true}, "view": true}, "clearances": {"security_course": {"edit": false, "view": true}, "security_clearance": {"edit": false, "view": true}, "edit": false, "medical_clearance": {"edit": false, "view": true}, "id": {"edit": false, "view": true}, "view": true}}}}} \ No newline at end of file diff --git a/src/etools/applications/t2f/permissions.py b/src/etools/applications/t2f/permissions.py index 0830f2c2d1..46d0497f72 100644 --- a/src/etools/applications/t2f/permissions.py +++ b/src/etools/applications/t2f/permissions.py @@ -5797,7 +5797,7 @@ ('edit', 'travel', 'cost_summary'): False, ('edit', 'travel', 'currency'): False, ('edit', 'travel', 'deductions'): False, - ('edit', 'travel', 'end_date'): True, + ('edit', 'travel', 'end_date'): False, ('edit', 'travel', 'expenses'): False, ('edit', 'travel', 'id'): False, ('edit', 'travel', 'international_travel'): False, @@ -5807,7 +5807,7 @@ ('edit', 'travel', 'reference_number'): False, ('edit', 'travel', 'report'): False, ('edit', 'travel', 'section'): False, - ('edit', 'travel', 'start_date'): True, + ('edit', 'travel', 'start_date'): False, ('edit', 'travel', 'status'): False, ('edit', 'travel', 'supervisor'): False, ('edit', 'travel', 'ta_required'): False, diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index c638cfdcd1..7c0befe796 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -8,6 +8,7 @@ class TravelActivityExportSerializer(serializers.Serializer): reference_number = serializers.CharField(source='travel.reference_number', read_only=True) traveler = serializers.CharField(source='travel.traveler.get_full_name', read_only=True) + purpose = serializers.CharField(source='travel.purpose', read_only=True) section = serializers.CharField(source='travel.section.name', read_only=True) office = serializers.CharField(source='travel.office.name', read_only=True) status = serializers.CharField(source='travel.status', read_only=True) @@ -30,6 +31,7 @@ class Meta: fields = ( 'reference_number', 'traveler', + 'purpose', 'office', 'section', 'status', diff --git a/src/etools/applications/t2f/tests/test_commands.py b/src/etools/applications/t2f/tests/test_commands.py index ccdc653b12..1242a0a9af 100644 --- a/src/etools/applications/t2f/tests/test_commands.py +++ b/src/etools/applications/t2f/tests/test_commands.py @@ -13,18 +13,3 @@ def test_command(self): mock_country = Mock(return_value=Country.objects.first()) with patch(COUNTRY_PATH, mock_country): call_command("et2f_init", "admin", "123") - - def test_command_load_users(self): - mock_country = Mock(return_value=Country.objects.first()) - with patch(COUNTRY_PATH, mock_country): - call_command("et2f_init", "admin", "123", "--with_users") - - def test_command_load_offices(self): - mock_country = Mock(return_value=Country.objects.first()) - with patch(COUNTRY_PATH, mock_country): - call_command("et2f_init", "admin", "123", "--with_offices") - - def test_command_load_partners(self): - mock_country = Mock(return_value=Country.objects.first()) - with patch(COUNTRY_PATH, mock_country): - call_command("et2f_init", "admin", "123", "--with_partners") diff --git a/src/etools/applications/t2f/tests/test_exports.py b/src/etools/applications/t2f/tests/test_exports.py index b6a0ff182b..3b722c7499 100644 --- a/src/etools/applications/t2f/tests/test_exports.py +++ b/src/etools/applications/t2f/tests/test_exports.py @@ -87,6 +87,7 @@ def test_activity_export(self): travel_1 = TravelFactory(reference_number='2016/1000', traveler=user_joe_smith, + purpose='Workshop', office=office, supervisor=supervisor, section=section_health, @@ -96,6 +97,7 @@ def test_activity_export(self): travel_2 = TravelFactory(reference_number='2016/1211', supervisor=supervisor, traveler=user_alice_carter, + purpose='Mission', office=office, section=section_education, start_date=datetime.date(2017, 11, 8), @@ -166,6 +168,7 @@ def test_activity_export(self): self.assertEqual(rows[0], [ 'reference_number', 'traveler', + 'purpose', 'office', 'section', 'status', @@ -186,6 +189,7 @@ def test_activity_export(self): self.assertEqual(rows[1], [ '2016/1000', 'Joe Smith', + 'Workshop', 'Budapest', 'Health', 'planned', @@ -206,6 +210,7 @@ def test_activity_export(self): self.assertEqual(rows[2], [ '2016/1000', 'Joe Smith', + 'Workshop', 'Budapest', 'Health', 'planned', @@ -226,6 +231,7 @@ def test_activity_export(self): self.assertEqual(rows[3], [ '2016/1000', 'Joe Smith', + 'Workshop', 'Budapest', 'Health', 'planned', @@ -246,6 +252,7 @@ def test_activity_export(self): self.assertEqual(rows[4], [ '2016/1211', 'Alice Carter', + 'Mission', 'Budapest', 'Education', 'planned', diff --git a/src/etools/applications/t2f/tests/test_permission_matrix.py b/src/etools/applications/t2f/tests/test_permission_matrix.py index a1caf4a9bc..d2a0e4494a 100644 --- a/src/etools/applications/t2f/tests/test_permission_matrix.py +++ b/src/etools/applications/t2f/tests/test_permission_matrix.py @@ -7,8 +7,7 @@ from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.publics.tests.factories import PublicsCurrencyFactory, PublicsDSARegionFactory -from etools.applications.t2f import UserTypes -from etools.applications.t2f.helpers.permission_matrix import FakePermissionMatrix, get_user_role_list +from etools.applications.t2f.helpers.permission_matrix import FakePermissionMatrix, get_user_role_list, UserTypes from etools.applications.t2f.models import ModeOfTravel, Travel, TravelType from etools.applications.t2f.tests.factories import TravelFactory from etools.applications.users.tests.factories import UserFactory diff --git a/src/etools/applications/t2f/views/exports.py b/src/etools/applications/t2f/views/exports.py index f05408504d..bedc325758 100644 --- a/src/etools/applications/t2f/views/exports.py +++ b/src/etools/applications/t2f/views/exports.py @@ -48,7 +48,7 @@ class TravelActivityExport(QueryStringFilterMixin, ExportBaseView): ('f_location', 'locations__pk__in'), ) - class SimpleDTO(object): + class SimpleDTO: def __init__(self, travel, activity): self.travel = travel self.activity = activity diff --git a/src/etools/applications/tpm/admin.py b/src/etools/applications/tpm/admin.py index 474b6e6a07..e9f615921b 100644 --- a/src/etools/applications/tpm/admin.py +++ b/src/etools/applications/tpm/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from etools.applications.publics.admin import AdminListMixin from etools.applications.tpm import forms, models +from etools.libraries.djangolib.admin import AdminListMixin @admin.register(models.TPMActivity) diff --git a/src/etools/applications/tpm/filters.py b/src/etools/applications/tpm/filters.py index 2a09dade09..13a96b1c38 100644 --- a/src/etools/applications/tpm/filters.py +++ b/src/etools/applications/tpm/filters.py @@ -19,6 +19,10 @@ def filter_queryset(self, request, queryset, view): class TPMVisitFilter(filters.FilterSet): + tpm_activities__offices__in = filters.BaseInFilter( + field_name="tpm_activities__offices", + ) + class Meta: model = TPMVisit fields = { @@ -26,7 +30,7 @@ class Meta: 'tpm_activities__section': ['exact', 'in'], 'tpm_activities__partner': ['exact', 'in'], 'tpm_activities__locations': ['exact'], - 'tpm_activities__offices': ['exact'], + 'tpm_activities__offices': ['exact', 'in'], 'tpm_activities__cp_output': ['exact', 'in'], 'tpm_activities__intervention': ['exact'], 'tpm_activities__date': ['exact', 'lte', 'gte', 'gt', 'lt'], diff --git a/src/etools/applications/tpm/management/commands/update_tpm_permissions.py b/src/etools/applications/tpm/management/commands/update_tpm_permissions.py index 9826fe57db..14fdc3875d 100644 --- a/src/etools/applications/tpm/management/commands/update_tpm_permissions.py +++ b/src/etools/applications/tpm/management/commands/update_tpm_permissions.py @@ -34,6 +34,7 @@ class Command(BaseCommand): 'tpm.tpmvisit.locations', 'tpm.tpmvisit.sections', 'tpm.tpmvisit.unicef_focal_points', + 'tpm.tpmvisit.tpm_partner_focal_points', ] status_dates = [ diff --git a/src/etools/applications/tpm/signals.py b/src/etools/applications/tpm/signals.py index 2e277d9b1c..6b99c04361 100644 --- a/src/etools/applications/tpm/signals.py +++ b/src/etools/applications/tpm/signals.py @@ -3,18 +3,11 @@ from django.db.models.signals import post_delete, post_save from django.dispatch import receiver -from etools.applications.tpm.models import ThirdPartyMonitor, TPMActionPoint, TPMVisit +from etools.applications.tpm.models import TPMActionPoint, TPMVisit from etools.applications.tpm.tpmpartners.models import TPMPartnerStaffMember from etools.applications.users.models import Country -@receiver(post_save, sender=TPMPartnerStaffMember) -def create_user_receiver(instance, created, **kwargs): - if created: - instance.user.groups.add(ThirdPartyMonitor.as_group()) - instance.user.profile.countries_available.add(connection.tenant) - - @receiver(post_delete, sender=TPMPartnerStaffMember) def delete_user_receiver(instance, **kwargs): user = instance.user diff --git a/src/etools/applications/tpm/tests/base.py b/src/etools/applications/tpm/tests/base.py index 2615ee5944..1c731cbe3c 100644 --- a/src/etools/applications/tpm/tests/base.py +++ b/src/etools/applications/tpm/tests/base.py @@ -5,7 +5,7 @@ from etools.libraries.djangolib.models import GroupWrapper -class TPMTestCaseMixin(object): +class TPMTestCaseMixin: @classmethod def setUpTestData(cls): call_command('update_tpm_permissions', verbosity=0) diff --git a/src/etools/applications/tpm/tests/test_models.py b/src/etools/applications/tpm/tests/test_models.py index 685a7f65bf..f9e300de8b 100644 --- a/src/etools/applications/tpm/tests/test_models.py +++ b/src/etools/applications/tpm/tests/test_models.py @@ -1,12 +1,10 @@ from django.contrib.auth import get_user_model -from django.core import mail from django.core.management import call_command from unicef_attachments.utils import get_denormalize_func from etools.applications.attachments.tests.factories import AttachmentFactory from etools.applications.core.tests.cases import BaseTenantTestCase -from etools.applications.tpm.models import ThirdPartyMonitor from etools.applications.tpm.tests.factories import TPMPartnerFactory, TPMPartnerStaffMemberFactory, TPMVisitFactory @@ -53,15 +51,6 @@ def setUpTestData(cls): cls.firm = TPMPartnerFactory() call_command('update_notifications') - def test_signal(self): - ThirdPartyMonitor.invalidate_cache() - - staff_member = TPMPartnerStaffMemberFactory(tpm_partner=self.firm) - - self.assertIn(ThirdPartyMonitor.name, staff_member.user.groups.values_list('name', flat=True)) - - self.assertEqual(len(mail.outbox), 0) - def test_post_delete(self): staff_member = TPMPartnerStaffMemberFactory(tpm_partner=self.firm) staff_member.delete() diff --git a/src/etools/applications/tpm/tests/test_views.py b/src/etools/applications/tpm/tests/test_views.py index fd63b3e366..2b4e658143 100644 --- a/src/etools/applications/tpm/tests/test_views.py +++ b/src/etools/applications/tpm/tests/test_views.py @@ -21,7 +21,14 @@ from etools.applications.reports.tests.factories import SectionFactory from etools.applications.tpm.models import ThirdPartyMonitor, TPMVisit from etools.applications.tpm.tests.base import TPMTestCaseMixin -from etools.applications.tpm.tests.factories import _FUZZY_END_DATE, TPMPartnerFactory, TPMUserFactory, TPMVisitFactory +from etools.applications.tpm.tests.factories import ( + _FUZZY_END_DATE, + OfficeFactory, + TPMActivityFactory, + TPMPartnerFactory, + TPMUserFactory, + TPMVisitFactory, +) from etools.applications.users.tests.factories import UserFactory from etools.libraries.djangolib.tests.utils import TestExportMixin @@ -98,6 +105,68 @@ def test_list_view_filter_multiple(self): } ) + def test_list_view_filter_office_single(self): + staff = self.tpm_user.tpmpartners_tpmpartnerstaffmember + visit = TPMVisitFactory( + status=TPMVisit.ASSIGNED, + tpm_partner=staff.tpm_partner, + tpm_partner_focal_points=[staff] + ) + office = OfficeFactory() + TPMActivityFactory(tpm_visit=visit, offices=[office]) + TPMVisitFactory( + status=TPMVisit.ASSIGNED, + tpm_partner=staff.tpm_partner, + tpm_partner_focal_points=[staff] + ) + self.assertIn( + visit, + TPMVisit.objects.filter(tpm_activities__offices=office), + ) + self._test_list_view( + self.tpm_user, + [visit], + filters={"tpm_activities__offices": office.pk} + ) + + def test_list_view_filter_office_multiple(self): + staff = self.tpm_user.tpmpartners_tpmpartnerstaffmember + visit_1 = TPMVisitFactory( + status=TPMVisit.ASSIGNED, + tpm_partner=staff.tpm_partner, + tpm_partner_focal_points=[staff] + ) + office_1 = OfficeFactory() + TPMActivityFactory(tpm_visit=visit_1, offices=[office_1]) + visit_2 = TPMVisitFactory( + status=TPMVisit.ASSIGNED, + tpm_partner=staff.tpm_partner, + tpm_partner_focal_points=[staff] + ) + office_2 = OfficeFactory() + TPMActivityFactory(tpm_visit=visit_2, offices=[office_2]) + TPMVisitFactory( + status=TPMVisit.ASSIGNED, + tpm_partner=staff.tpm_partner, + tpm_partner_focal_points=[staff] + ) + self.assertEqual( + list(TPMVisit.objects.filter( + tpm_activities__offices__in=[office_1, office_2] + ).all()), + [visit_1, visit_2], + ) + self._test_list_view( + self.tpm_user, + [visit_1, visit_2], + filters={ + "tpm_activities__offices__in": ",".join([ + str(office_1.pk), + str(office_2.pk), + ]), + } + ) + def test_list_view_without_tpm_organization(self): user = UserFactory() user.groups.add(ThirdPartyMonitor.as_group()) diff --git a/src/etools/applications/tpm/tpmpartners/admin.py b/src/etools/applications/tpm/tpmpartners/admin.py index cc6c8d5832..ce3b120fad 100644 --- a/src/etools/applications/tpm/tpmpartners/admin.py +++ b/src/etools/applications/tpm/tpmpartners/admin.py @@ -36,6 +36,7 @@ class TPMPartnerStaffMemberAdmin(admin.ModelAdmin): list_filter = ['receive_tpm_notifications', 'user__is_active', 'tpm_partner'] search_fields = ['user__email', 'user__first_name', 'user__last_name', 'user__profile__phone_number', 'tpm_partner__name'] + raw_id_fields = ('user',) def email(self, obj): return obj.user.email diff --git a/src/etools/applications/tpm/tpmpartners/synchronizers.py b/src/etools/applications/tpm/tpmpartners/synchronizers.py index 3fbd2b09d1..0a333bb0a2 100644 --- a/src/etools/applications/tpm/tpmpartners/synchronizers.py +++ b/src/etools/applications/tpm/tpmpartners/synchronizers.py @@ -1,9 +1,10 @@ from collections import OrderedDict from copy import deepcopy +from unicef_vision.synchronizers import ManualVisionSynchronizer + from etools.applications.publics.models import Country from etools.applications.tpm.tpmpartners.models import TPMPartner -from etools.applications.vision.synchronizers import ManualVisionSynchronizer def _get_country_name(value): diff --git a/src/etools/applications/tpm/tpmpartners/tasks.py b/src/etools/applications/tpm/tpmpartners/tasks.py index 7f77308515..dfc0ac9d45 100644 --- a/src/etools/applications/tpm/tpmpartners/tasks.py +++ b/src/etools/applications/tpm/tpmpartners/tasks.py @@ -1,9 +1,9 @@ from celery.utils.log import get_task_logger +from unicef_vision.exceptions import VisionException from etools.applications.tpm.tpmpartners.models import TPMPartner from etools.applications.tpm.tpmpartners.synchronizers import TPMPartnerSynchronizer from etools.applications.users.models import Country -from etools.applications.vision.exceptions import VisionException from etools.config.celery import app logger = get_task_logger(__name__) diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index e81b87c45c..321ff81683 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -221,9 +221,11 @@ class TPMStaffMembersViewSet( def perform_create(self, serializer, **kwargs): self.check_serializer_permissions(serializer, edit=True) - instance = serializer.save(tpm_partner=self.get_parent_object(), **kwargs) - instance.user.profile.country = self.request.user.profile.country + if not instance.user.profile.country: + instance.user.profile.country = self.request.user.profile.country + instance.user.profile.countries_available.add(self.request.user.profile.country) + instance.user.groups.add(ThirdPartyMonitor.as_group()) instance.user.profile.save() @action(detail=False, methods=['get'], url_path='export', renderer_classes=(TPMPartnerContactsCSVRenderer,)) diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index 3204577354..9e95de509a 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse +from django_tenants.admin import TenantAdminMixin from django_tenants.utils import get_public_schema_name from etools.applications.hact.tasks import update_hact_for_country, update_hact_values @@ -234,7 +235,7 @@ def get_readonly_fields(self, request, obj=None): return fields -class CountryAdmin(admin.ModelAdmin): +class CountryAdmin(TenantAdminMixin, admin.ModelAdmin): change_form_template = 'admin/users/country/change_form.html' def has_add_permission(self, request): @@ -297,7 +298,7 @@ def execute_sync(country_pk, synchronizer): if country.schema_name == get_public_schema_name(): vision_sync_task(synchronizers=[synchronizer, ]) else: - sync_handler.delay(country.name, synchronizer) + sync_handler.delay(country.business_area_code, synchronizer) return HttpResponseRedirect(reverse('admin:users_country_change', args=[country.pk])) def update_hact(self, request, pk): @@ -306,7 +307,7 @@ def update_hact(self, request, pk): update_hact_values() messages.info(request, "HACT update has been scheduled for all countries") else: - update_hact_for_country.delay(country_name=country.name) + update_hact_for_country.delay(business_area_code=country.business_area_code) messages.info(request, "HACT update has been started for %s" % country.name) return HttpResponseRedirect(reverse('admin:users_country_change', args=[country.pk])) diff --git a/src/etools/applications/users/forms.py b/src/etools/applications/users/forms.py deleted file mode 100644 index cb9d9c6b41..0000000000 --- a/src/etools/applications/users/forms.py +++ /dev/null @@ -1,28 +0,0 @@ -from django import forms -from django.db import connection - -from etools.applications.users.models import Office, UserProfile - - -class ProfileForm(forms.ModelForm): - office = forms.ModelChoiceField( - Office.objects.all(), - empty_label='Office', - widget=forms.Select(attrs={'class': 'form-control input-sm'}) - ) - job_title = forms.CharField( - max_length=255, - widget=forms.TextInput(attrs={'class': 'form-control'}) - ) - phone_number = forms.CharField( - max_length=255, - widget=forms.TextInput(attrs={'class': 'form-control'}) - ) - - class Meta: - model = UserProfile - exclude = ['user', ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['office'].queryset = connection.tenant.offices.all() diff --git a/src/etools/applications/users/management/commands/add_country.py b/src/etools/applications/users/management/commands/add_country.py index ac6ffdf768..b9b5f2c734 100644 --- a/src/etools/applications/users/management/commands/add_country.py +++ b/src/etools/applications/users/management/commands/add_country.py @@ -20,15 +20,17 @@ def handle(self, *args, **options): name = options['country_name'] slug = name.lower().replace(' ', '-').strip() usd = Currency.objects.get(code='USD') + schema_name = name.lower().replace(' ', '_').strip() country = Country.objects.create( - schema_name=name.lower().replace(' ', '_').strip(), + schema_name=schema_name, name=name, local_currency=usd, ) get_tenant_domain_model().objects.create(domain='{}.etools.unicef.org'.format(slug), tenant=country) - call_command('init-result-type', schema=slug) - call_command('init-partner-file-type', schema=slug) - connection.set_schema(slug) + call_command('init-result-type', schema=schema_name) + call_command('init-partner-file-type', schema=schema_name) + call_command('init-attachment-file-types', schema=schema_name) + connection.set_schema(schema_name) call_command('loaddata', 'attachments_file_types') call_command('loaddata', 'audit_risks_blueprints') for user in get_user_model().objects.filter(is_superuser=True): diff --git a/src/etools/applications/users/migrations/0011_auto_20190425_1838.py b/src/etools/applications/users/migrations/0011_auto_20190425_1838.py new file mode 100644 index 0000000000..d25c20050b --- /dev/null +++ b/src/etools/applications/users/migrations/0011_auto_20190425_1838.py @@ -0,0 +1,21 @@ +# Generated by Django 2.1.8 on 2019-04-25 18:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0010_auto_20190423_1920'), + ] + + operations = [ + migrations.RemoveField( + model_name='country', + name='threshold_tae_usd', + ), + migrations.RemoveField( + model_name='country', + name='threshold_tre_usd', + ), + ] diff --git a/src/etools/applications/users/migrations/0012_auto_20190513_1804.py b/src/etools/applications/users/migrations/0012_auto_20190513_1804.py new file mode 100644 index 0000000000..8a0450f66a --- /dev/null +++ b/src/etools/applications/users/migrations/0012_auto_20190513_1804.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.8 on 2019-05-13 18:04 + +from django.db import migrations, models +import django_tenants.postgresql_backend.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0011_auto_20190425_1838'), + ] + + operations = [ + migrations.AlterField( + model_name='country', + name='schema_name', + field=models.CharField(db_index=True, max_length=63, unique=True, validators=[django_tenants.postgresql_backend.base._check_schema_name]), + ), + ] diff --git a/src/etools/applications/users/models.py b/src/etools/applications/users/models.py index 96be817b30..7cd6baaa0d 100644 --- a/src/etools/applications/users/models.py +++ b/src/etools/applications/users/models.py @@ -111,11 +111,6 @@ class Country(TenantMixin): # TODO: rename the related name as it's inappropriate for relating offices to countries.. should be office_countries offices = models.ManyToManyField('Office', related_name='offices', verbose_name=_('Offices'), blank=True) - threshold_tre_usd = models.DecimalField(max_digits=20, decimal_places=4, blank=True, null=True, - verbose_name=_('Threshold TRE (USD)')) - threshold_tae_usd = models.DecimalField(max_digits=20, decimal_places=4, blank=True, null=True, - verbose_name=_('Threshold TAE (USD)')) - def __str__(self): return self.name @@ -253,7 +248,7 @@ class UserProfile(models.Model): oic = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, verbose_name=_('OIC'), null=True, blank=True) # related oic_set - # TODO: figure this out when we need to autmatically map to groups + # TODO: figure this out when we need to automatically map to groups # vision_roles = ArrayField(models.CharField(max_length=20, blank=True, choices=VISION_ROLES), # blank=True, null=True) diff --git a/src/etools/applications/users/synchronizers.py b/src/etools/applications/users/synchronizers.py index 503b857f28..594d91897f 100644 --- a/src/etools/applications/users/synchronizers.py +++ b/src/etools/applications/users/synchronizers.py @@ -1,10 +1,10 @@ import json from etools.applications.users.models import Country -from etools.applications.vision.vision_data_synchronizer import VisionDataSynchronizer +from etools.applications.vision.synchronizers import VisionDataTenantSynchronizer -class CountryLongNameSync(VisionDataSynchronizer): +class CountryLongNameSync(VisionDataTenantSynchronizer): ENDPOINT = 'GetBusinessAreaList_JSON' GLOBAL_CALL = True diff --git a/src/etools/applications/users/templates/admin/users/country/change_form.html b/src/etools/applications/users/templates/admin/users/country/change_form.html index 80b6948cf4..505af2a7e5 100644 --- a/src/etools/applications/users/templates/admin/users/country/change_form.html +++ b/src/etools/applications/users/templates/admin/users/country/change_form.html @@ -1,4 +1,5 @@ -{% extends "admin/change_form.html" %}{% load i18n etools %} +{% extends "admin/django_tenants/tenant/change_form.html" %} +{% load i18n etools %} {% block object-tools-items %} {{ block.super }}
  • {% trans "API Partner" %}
  • @@ -16,20 +17,4 @@
  • {% trans "Sync DCT" %}
  • {% trans "Update HACT" %}
  • - {% endblock object-tools-items %} - - -{% block submit_buttons_bottom %} -{% if request.tenant.schema_name == 'public' or request.tenant.schema_name == original.schema_name %} -{{ block.super }} -{% else %} -
    - -

    Can't update tenant outside it's own schema or the public schema. Current schema is {{request.tenant.name}}.

    - - -
    - -{% endif %} -{% endblock %} \ No newline at end of file diff --git a/src/etools/applications/users/views_v3.py b/src/etools/applications/users/views_v3.py index ae18273a55..062409334f 100644 --- a/src/etools/applications/users/views_v3.py +++ b/src/etools/applications/users/views_v3.py @@ -65,6 +65,7 @@ class UsersListAPIView(QueryStringFilterMixin, ListAPIView): filters = ( ('group', 'groups__name__in'), + ('is_active', 'is_active'), ) def get_queryset(self, pk=None): diff --git a/src/etools/applications/vision/admin.py b/src/etools/applications/vision/admin.py index 89eb0217f7..b24763378a 100644 --- a/src/etools/applications/vision/admin.py +++ b/src/etools/applications/vision/admin.py @@ -1,39 +1,15 @@ from django.contrib import admin -from etools.applications.vision.models import VisionSyncLog +from unicef_vision.admin import VisionLoggerAdmin +from etools.applications.vision.models import VisionSyncLog -class VisionSyncLogAdmin(admin.ModelAdmin): - def has_add_permission(self, request): - return False +@admin.register(VisionSyncLog) +class VisionSyncLogAdmin(VisionLoggerAdmin): change_form_template = 'admin/vision/vision_log/change_form.html' - list_filter = ( - 'country', - 'handler_name', - 'successful', - 'date_processed', - ) - list_display = ( - 'country', - 'handler_name', - 'total_records', - 'total_processed', - 'successful', - 'date_processed', - ) - readonly_fields = ( - 'country', - 'details', - 'handler_name', - 'total_records', - 'total_processed', - 'successful', - 'exception_message', - 'date_processed', - ) - - -admin.site.register(VisionSyncLog, VisionSyncLogAdmin) + list_filter = VisionLoggerAdmin.list_filter + ('country',) + list_display = VisionLoggerAdmin.list_display + ('country',) + readonly_fields = VisionLoggerAdmin.readonly_fields + ('country',) diff --git a/src/etools/applications/vision/client.py b/src/etools/applications/vision/client.py deleted file mode 100755 index 0f8d9dc4fb..0000000000 --- a/src/etools/applications/vision/client.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python - -import argparse -import json -import logging -import os -from urllib.parse import urljoin - -from django.conf import settings - -import requests -from requests.auth import HTTPDigestAuth - -logger = logging.getLogger(__name__) - - -class VisionAPIClient(object): - """ - """ - - def __init__(self, - username=None, - password=None, # TODO: make configurable - base_url=settings.VISION_URL): - self.base_url = base_url - if username and password: - self.auth = HTTPDigestAuth(username, password) - - def build_path(self, path=None): - """ Builds the full path to the service. - Args: - path (string): The part of the path you want to append - to the base url. - Returns: - A string containing the full path to the endpoint. - e.g if the base_url was "http://woo.com" and the path was - "databases" it would return "http://woo.com/databases/" - """ - if path is None: - return self.base_url - return urljoin( - self.base_url, os.path.normpath(path), - ) - - def make_request(self, path): - - response = requests.get( - self.build_path(path), - auth=getattr(self, 'auth', ()), - ) - return response - - def call_command(self, command_type, **properties): - - payload = json.dumps( - { - 'type': command_type, - 'command': { - 'properties': properties - } - } - ) - - response = requests.post( - self.build_path('command'), - headers={'cache-control': 'application/json'}, - auth=getattr(self, 'auth', ()), - data=payload, - ) - return response - - def get_business_areas(self): - return self.make_request('GetBusinessAreaList_JSON').json() - - def get_programme_structure(self, business_area): - return self.make_request('GetProgrammeStructureList_JSON/{}'.format(business_area)).json() - - -def main(): - """ - Main method for command line usage - """ - parser = argparse.ArgumentParser( - description='VISION API Python Client' - ) - - parser.add_argument('-U', '--username', - type=str, - default='', - help='Optional username for authentication') - parser.add_argument('-P', '--password', - type=str, - default='', - help='Optional password for authentication') - - args = parser.parse_args() - - try: - client = VisionAPIClient( - username=args.username, - password=args.password, - ) - - logger.info(client.get_business_areas()) - - except Exception: - logger.exception("Exception in vision client") - - -if __name__ == '__main__': - main() diff --git a/src/etools/applications/vision/exceptions.py b/src/etools/applications/vision/exceptions.py deleted file mode 100644 index bcd57318d0..0000000000 --- a/src/etools/applications/vision/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class VisionException(Exception): - """Vision Generic Exception""" diff --git a/src/etools/applications/vision/migrations/0002_fix_null_values.py b/src/etools/applications/vision/migrations/0002_fix_null_values.py index 43ebc00928..3c4fb58a93 100644 --- a/src/etools/applications/vision/migrations/0002_fix_null_values.py +++ b/src/etools/applications/vision/migrations/0002_fix_null_values.py @@ -1,6 +1,7 @@ from django.db import migrations + class Migration(migrations.Migration): dependencies = [ diff --git a/src/etools/applications/vision/migrations/0004_visionsynclog_business_area_code.py b/src/etools/applications/vision/migrations/0004_visionsynclog_business_area_code.py new file mode 100644 index 0000000000..486ad48a04 --- /dev/null +++ b/src/etools/applications/vision/migrations/0004_visionsynclog_business_area_code.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.9 on 2019-02-06 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vision', '0003_make_not_nullable'), + ] + + operations = [ + migrations.AddField( + model_name='visionsynclog', + name='business_area_code', + field=models.CharField(blank=True, max_length=10, null=True, verbose_name='Business Area Code'), + ), + ] diff --git a/src/etools/applications/vision/models.py b/src/etools/applications/vision/models.py index 56a11d49b4..79b718e9fa 100644 --- a/src/etools/applications/vision/models.py +++ b/src/etools/applications/vision/models.py @@ -1,24 +1,13 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from etools.applications.users.models import Country - +from unicef_vision.models import AbstractVisionLog -class VisionSyncLog(models.Model): - """ - Represents a sync log for Vision SAP sync +from etools.applications.users.models import Country - Relates to :model:`users.Country` - """ +class VisionSyncLog(AbstractVisionLog): country = models.ForeignKey(Country, verbose_name=_('Country'), on_delete=models.CASCADE) - handler_name = models.CharField(max_length=50, verbose_name=_('Handler Name')) - total_records = models.IntegerField(default=0, verbose_name=_('Total Records')) - total_processed = models.IntegerField(default=0, verbose_name=_('Total Processed')) - successful = models.BooleanField(default=False, verbose_name=_('Successful')) - details = models.CharField(max_length=2048, blank=True, default='', verbose_name=_('Details')) - exception_message = models.TextField(blank=True, default='', verbose_name=_('Exception Message')) - date_processed = models.DateTimeField(auto_now=True, verbose_name=_('Date Processed')) def __str__(self): return '{0.country} {0.date_processed}:{0.successful} {0.total_processed}'.format(self) diff --git a/src/etools/applications/vision/synchronizers.py b/src/etools/applications/vision/synchronizers.py index 6b7903c2e5..9fa5fc14e3 100644 --- a/src/etools/applications/vision/synchronizers.py +++ b/src/etools/applications/vision/synchronizers.py @@ -1,157 +1,26 @@ - -import json import logging -import types -from collections import OrderedDict from django.db import connection -from django.db.models import NOT_PROVIDED - -from etools.applications.vision.exceptions import VisionException -from etools.applications.vision.utils import wcf_json_date_as_datetime -from etools.applications.vision.vision_data_synchronizer import VisionDataLoader, VisionDataSynchronizer - -logger = logging.getLogger(__name__) - - -class Empty(object): - pass - - -class ManualDataLoader(VisionDataLoader): - """ - Can be used to sync single objects from VISION - url templates: - /endpoint if no country or object_number - /endpoint/country if no object number provided - /endpoint/object_number else - """ - - def __init__(self, country=None, endpoint=None, object_number=None): - if not object_number: - super().__init__(country=country, endpoint=endpoint) - else: - if endpoint is None: - raise VisionException('You must set the ENDPOINT name') - self.url = '{}/{}/{}'.format( - self.URL, - endpoint, - object_number - ) - - -class MultiModelDataSynchronizer(VisionDataSynchronizer): - MODEL_MAPPING = {} - MAPPING = OrderedDict() - DATE_FIELDS = [] - DEFAULTS = {} - FIELD_HANDLERS = {} - def _convert_records(self, records): - if isinstance(records, list): - return records - try: - return json.loads(records) - except ValueError: - return [] +from django_tenants.utils import get_tenant_model +from unicef_vision.synchronizers import VisionDataSynchronizer - def _get_field_value(self, field_name, field_json_code, json_item, model): - result = None +from etools.applications.vision.models import VisionSyncLog - if field_json_code in self.DATE_FIELDS: - # parsing field as date - return wcf_json_date_as_datetime(json_item[field_json_code]) - elif field_name in self.MODEL_MAPPING.keys(): - # this is related model, so we need to fetch somehow related object. - related_model = self.MODEL_MAPPING[field_name] - - if isinstance(related_model, types.FunctionType): - # callable provided, object should be returned from it - result = related_model(data=json_item, key_field=field_json_code) - else: - # model class provided, related object can be fetched with query by field - # analogue of field_json_code - reversed_dict = dict(zip( - self.MAPPING[field_name].values(), - self.MAPPING[field_name].keys() - )) - result = related_model.objects.get(**{ - reversed_dict[field_json_code]: json_item.get(field_json_code, None) - }) - else: - # field can be used as it is without custom mappings. if field has default, it should be used - result = json_item.get(field_json_code, Empty) - if result is Empty: - # try to get default for field - field_default = model._meta.get_field(field_name).default - if field_default is not NOT_PROVIDED: - result = field_default - - # additional logic on field may be applied - value_handler = self.FIELD_HANDLERS.get( - {y: x for x, y in self.MODEL_MAPPING.items()}.get(model), {} - ).get(field_name, None) - if value_handler: - result = value_handler(result) - return result - - def _process_record(self, json_item): - try: - for model_name, model in self.MODEL_MAPPING.items(): - mapped_item = dict( - [(field_name, self._get_field_value(field_name, field_json_code, json_item, model)) - for field_name, field_json_code in self.MAPPING[model_name].items()] - ) - kwargs = dict( - [(field_name, value) for field_name, value in mapped_item.items() - if model._meta.get_field(field_name).unique] - ) - - if not kwargs: - for fields in model._meta.unique_together: - if all(field in mapped_item.keys() for field in fields): - unique_fields = fields - break - - kwargs = { - field: mapped_item[field] for field in unique_fields - } - - defaults = dict( - [(field_name, value) for field_name, value in mapped_item.items() - if field_name not in kwargs.keys()] - ) - defaults.update(self.DEFAULTS.get(model, {})) - model.objects.update_or_create( - defaults=defaults, **kwargs - ) - except Exception: - logger.warning('Exception processing record', exc_info=True) - - def _save_records(self, records): - processed = 0 - filtered_records = self._filter_records(records) - - for record in filtered_records: - self._process_record(record) - processed += 1 - return processed - - -class ManualVisionSynchronizer(MultiModelDataSynchronizer): - LOADER_CLASS = ManualDataLoader - LOADER_EXTRA_KWARGS = ['object_number', ] +logger = logging.getLogger(__name__) - def __init__(self, country=None, object_number=None): - self.object_number = object_number - if not object_number: - super().__init__(country=country) - else: - if self.ENDPOINT is None: - raise VisionException('You must set the ENDPOINT name') +class VisionDataTenantSynchronizer(VisionDataSynchronizer): + LOGGER_CLASS = VisionSyncLog - self.country = country + def __init__(self, business_area_code=None, *args, **kwargs): + super().__init__(business_area_code, *args, **kwargs) + self.country = get_tenant_model().objects.get(business_area_code=self.business_area_code) + connection.set_tenant(self.country) - connection.set_tenant(country) - logger.info('Country is {}'.format(country.name)) + def logger_parameters(self): + return { + 'handler_name': self.__class__.__name__, + 'business_area_code': self.business_area_code, + 'country': self.country + } diff --git a/src/etools/applications/vision/tasks.py b/src/etools/applications/vision/tasks.py index da84599449..5f8fd234e0 100644 --- a/src/etools/applications/vision/tasks.py +++ b/src/etools/applications/vision/tasks.py @@ -2,12 +2,12 @@ from django.utils import timezone from celery.utils.log import get_task_logger +from unicef_vision.exceptions import VisionException from etools.applications.funds.synchronizers import FundCommitmentSynchronizer, FundReservationsSynchronizer from etools.applications.partners.synchronizers import DirectCashTransferSynchronizer, PartnerSynchronizer from etools.applications.reports.synchronizers import ProgrammeSynchronizer, RAMSynchronizer from etools.applications.users.models import Country -from etools.applications.vision.exceptions import VisionException from etools.config.celery import app, send_to_slack PUBLIC_SYNC_HANDLERS = {} @@ -27,7 +27,7 @@ @app.task -def vision_sync_task(country_name=None, synchronizers=SYNC_HANDLERS.keys()): +def vision_sync_task(business_area_code=None, synchronizers=SYNC_HANDLERS.keys()): """ Do the vision sync for all countries that have vision_sync_enabled=True, or just the named country. Defaults to SYNC_HANDLERS but a @@ -42,17 +42,17 @@ def vision_sync_task(country_name=None, synchronizers=SYNC_HANDLERS.keys()): country_filter_dict = { 'vision_sync_enabled': True } - if country_name: - country_filter_dict['name'] = country_name + if business_area_code: + country_filter_dict['business_area_code'] = business_area_code countries = Country.objects.filter(**country_filter_dict) - if not country_name or country_name == 'Global': + if not business_area_code or business_area_code == '0': # public schema for handler in global_synchronizers: - sync_handler.delay('Global', handler) + sync_handler.delay(business_area_code, handler) for country in countries: connection.set_tenant(country) for handler in tenant_synchronizers: - sync_handler.delay(country.name, handler) + sync_handler.delay(country.business_area_code, handler) country.vision_last_synced = timezone.now() country.save() @@ -65,28 +65,28 @@ def vision_sync_task(country_name=None, synchronizers=SYNC_HANDLERS.keys()): @app.task(bind=True, autoretry_for=(VisionException,), retry_kwargs={'max_retries': 1}) -def sync_handler(self, country_name, handler): +def sync_handler(self, business_area_code, handler): """ Run .sync() on one handler for one country. """ # Scheduled from vision_sync_task() (above). - logger.info('Starting vision sync handler {} for country {}'.format(handler, country_name)) + logger.info('Starting vision sync handler {} for country {}'.format(handler, business_area_code)) try: - country = Country.objects.get(name=country_name) + country = Country.objects.get(business_area_code=business_area_code) except Country.DoesNotExist: - logger.error("{} sync failed, Could not find a Country with this name: {}".format( - handler, country_name + logger.error("{} sync failed, Could not find a Country with this business area code: {}".format( + handler, business_area_code )) # No point in retrying if there's no such country else: try: - SYNC_HANDLERS[handler](country).sync() - logger.info("{} sync successfully for {}".format(handler, country.name)) + SYNC_HANDLERS[handler](country.business_area_code).sync() + logger.info("{} sync successfully for {} [{}]".format(handler, country.name, business_area_code)) except VisionException: # Catch and log the exception so we're aware there's a problem. logger.exception("{} sync failed, Country: {}".format( - handler, country_name + handler, business_area_code )) # The 'autoretry_for' in the task decorator tells Celery to # retry this a few times on VisionExceptions, so just re-raise it diff --git a/src/etools/applications/vision/tests/test_client.py b/src/etools/applications/vision/tests/test_client.py deleted file mode 100644 index 91f966e8f9..0000000000 --- a/src/etools/applications/vision/tests/test_client.py +++ /dev/null @@ -1,28 +0,0 @@ - -from django.test import SimpleTestCase - -from etools.applications.vision import client - - -class TestVisionClient(SimpleTestCase): - def setUp(self): - self.client = client.VisionAPIClient() - - def test_init(self): - c = client.VisionAPIClient() - self.assertTrue(c.base_url) - - def test_init_auth(self): - """Check that auth attribute if username and password provided""" - c = client.VisionAPIClient(username="test", password="123") - self.assertTrue(c.base_url) - self.assertIsInstance(c.auth, client.HTTPDigestAuth) - - def test_build_path_none(self): - """If no path provided, use base_url attribute""" - path = self.client.build_path() - self.assertEqual(path, self.client.base_url) - - def test_build_path(self): - path = self.client.build_path("api") - self.assertEqual(path, "{}/api".format(self.client.base_url)) diff --git a/src/etools/applications/vision/tests/test_models.py b/src/etools/applications/vision/tests/test_models.py deleted file mode 100644 index 68833bae71..0000000000 --- a/src/etools/applications/vision/tests/test_models.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.test import SimpleTestCase - -from etools.applications.users.tests.factories import CountryFactory -from etools.applications.vision.models import VisionSyncLog - - -class TestStrUnicode(SimpleTestCase): - """Ensure calling str() on model instances returns the right text.""" - - def test_vision_sync_log(self): - country = CountryFactory.build(name='M\xe9xico', schema_name='Mexico') - instance = VisionSyncLog(country=country) - self.assertTrue(str(instance).startswith('M\xe9xico')) diff --git a/src/etools/applications/vision/tests/test_synchronizers.py b/src/etools/applications/vision/tests/test_synchronizers.py deleted file mode 100644 index 89d8443bb2..0000000000 --- a/src/etools/applications/vision/tests/test_synchronizers.py +++ /dev/null @@ -1,418 +0,0 @@ -from django.conf import settings -from django.test import override_settings -from django.utils.timezone import now as django_now - -import mock - -from etools.applications.core.tests.cases import BaseTenantTestCase -from etools.applications.users.models import Country -from etools.applications.vision.exceptions import VisionException -from etools.applications.vision.models import VisionSyncLog -from etools.applications.vision.synchronizers import ManualDataLoader -from etools.applications.vision.vision_data_synchronizer import ( - VISION_NO_DATA_MESSAGE, - VisionDataLoader, - VisionDataSynchronizer, -) - -FAUX_VISION_URL = 'https://api.example.com/foo.svc/' -FAUX_VISION_USER = 'jane_user' -FAUX_VISION_PASSWORD = 'password123' - - -class _MySynchronizer(VisionDataSynchronizer): - """Bare bones synchronizer class. Exists because VisionDataSynchronizer is abstract; this is concrete but - does as little as possible. - """ - ENDPOINT = 'GetSomeStuff_JSON' - - def _convert_records(self, records): - pass - - def _save_records(self, records): - pass - - -class TestVisionDataLoader(BaseTenantTestCase): - """Exercise VisionDataLoader class""" - # Note - I don't understand why, but @override_settings(VISION_URL=FAUX_VISION_URL) doesn't work when I apply - # it at the TestCase class level instead of each individual test case. - - def _assertGetFundamentals(self, url, mock_requests, mock_get_response): - """Assert common things about the call to loader.get()""" - # Ensure requests.get() was called as expected - self.assertEqual(mock_requests.get.call_count, 1) - self.assertEqual(mock_requests.get.call_args[0], (url, )) - self.assertEqual(mock_requests.get.call_args[1], {'headers': {'Content-Type': 'application/json'}, - 'auth': (FAUX_VISION_USER, FAUX_VISION_PASSWORD), - 'verify': False}) - # Ensure response.json() was called as expected - self.assertEqual(mock_get_response.json.call_count, 1) - self.assertEqual(mock_get_response.json.call_args[0], tuple()) - self.assertEqual(mock_get_response.json.call_args[1], {}) - - def test_instantiation_no_country(self): - """Ensure I can create a loader without specifying a country""" - loader = VisionDataLoader(endpoint='GetSomeStuff_JSON') - self.assertEqual(loader.url, '{}/GetSomeStuff_JSON'.format(loader.URL)) - - def test_instantiation_with_country(self): - """Ensure I can create a loader that specifies a country""" - test_country = Country.objects.all()[0] - test_country.business_area_code = 'ABC' - test_country.save() - - loader = VisionDataLoader(country=test_country, endpoint='GetSomeStuff_JSON') - self.assertEqual(loader.url, '{}/GetSomeStuff_JSON/ABC'.format(loader.URL)) - - def test_instantiation_url_construction(self): - """Ensure loader URL is constructed correctly regardless of whether or not base URL ends with a slash""" - loader = VisionDataLoader(endpoint='GetSomeStuff_JSON') - self.assertEqual(loader.url, '{}/GetSomeStuff_JSON'.format(loader.URL)) - - @override_settings(VISION_URL=FAUX_VISION_URL) - @override_settings(VISION_USER=FAUX_VISION_USER) - @override_settings(VISION_PASSWORD=FAUX_VISION_PASSWORD) - @mock.patch('etools.applications.vision.vision_data_synchronizer.requests', spec=['get']) - def test_get_success_with_response(self, mock_requests): - """Test loader.get() when the response is 200 OK and data is returned""" - mock_get_response = mock.Mock(spec=['status_code', 'json']) - mock_get_response.status_code = 200 - mock_get_response.json = mock.Mock(return_value=[42]) - mock_requests.get = mock.Mock(return_value=mock_get_response) - - loader = VisionDataLoader(endpoint='GetSomeStuff_JSON') - response = loader.get() - - self._assertGetFundamentals(loader.url, mock_requests, mock_get_response) - - self.assertEqual(response, [42]) - - @override_settings(VISION_URL=FAUX_VISION_URL) - @override_settings(VISION_USER=FAUX_VISION_USER) - @override_settings(VISION_PASSWORD=FAUX_VISION_PASSWORD) - @mock.patch('etools.applications.vision.vision_data_synchronizer.requests', spec=['get']) - def test_get_success_no_response(self, mock_requests): - """Test loader.get() when the response is 200 OK but no data is returned""" - mock_get_response = mock.Mock(spec=['status_code', 'json']) - mock_get_response.status_code = 200 - mock_get_response.json = mock.Mock(return_value=VISION_NO_DATA_MESSAGE) - mock_requests.get = mock.Mock(return_value=mock_get_response) - - loader = VisionDataLoader(endpoint='GetSomeStuff_JSON') - response = loader.get() - - self._assertGetFundamentals(loader.url, mock_requests, mock_get_response) - - self.assertEqual(response, []) - - @override_settings(VISION_URL=FAUX_VISION_URL) - @override_settings(VISION_USER=FAUX_VISION_USER) - @override_settings(VISION_PASSWORD=FAUX_VISION_PASSWORD) - @mock.patch('etools.applications.vision.vision_data_synchronizer.requests', spec=['get']) - def test_get_failure(self, mock_requests): - """Test loader.get() when the response is something other than 200""" - # Note that in contrast to the other mock_get_response variables declared in this test case, this one - # doesn't have 'json' in the spec. I don't expect the loaderto access response.json during this test, so if - # it does this configuration ensures the test will fail. - mock_get_response = mock.Mock(spec=['status_code']) - mock_get_response.status_code = 401 - mock_requests.get = mock.Mock(return_value=mock_get_response) - - loader = VisionDataLoader(endpoint='GetSomeStuff_JSON') - with self.assertRaises(VisionException) as context_manager: - loader.get() - - # Assert that the status code is repeated in the message of the raised exception. - self.assertIn('401', str(context_manager.exception)) - - # Ensure get was called as normal. - self.assertEqual(mock_requests.get.call_count, 1) - self.assertEqual(mock_requests.get.call_args[0], (loader.url, )) - self.assertEqual(mock_requests.get.call_args[1], {'headers': {'Content-Type': 'application/json'}, - 'auth': (FAUX_VISION_USER, FAUX_VISION_PASSWORD), - 'verify': False}) - - -class TestVisionDataSynchronizerInit(BaseTenantTestCase): - """Exercise initialization of VisionDataSynchronizer class""" - - def test_instantiation_no_country(self): - """Ensure I can't create a synchronizer without specifying a country""" - with self.assertRaises(VisionException) as context_manager: - _MySynchronizer() - - self.assertEqual('Country is required', str(context_manager.exception)) - - def test_instantiation_no_endpoint(self): - """Ensure I can't create a synchronizer without specifying an endpoint""" - class _MyBadSynchronizer(_MySynchronizer): - """Synchronizer class that doesn't set self.ENDPOINT""" - ENDPOINT = None - - test_country = Country.objects.all()[0] - - with self.assertRaises(VisionException) as context_manager: - _MyBadSynchronizer(country=test_country) - - self.assertEqual('You must set the ENDPOINT name', str(context_manager.exception)) - - @mock.patch('etools.applications.vision.vision_data_synchronizer.connection', spec=['set_tenant']) - @mock.patch('etools.applications.vision.vision_data_synchronizer.logger.info') - def test_instantiation_positive(self, mock_logger_info, mock_connection): - """Exercise successfully creating a synchronizer""" - test_country = Country.objects.all()[0] - test_country.business_area_code = 'ABC' - test_country.save() - - _MySynchronizer(country=test_country) - - # Ensure tenant is set - self.assertEqual(mock_connection.set_tenant.call_count, 1) - self.assertEqual(mock_connection.set_tenant.call_args[0], (test_country, )) - self.assertEqual(mock_connection.set_tenant.call_args[1], {}) - - # Ensure msgs are logged - self.assertEqual(mock_logger_info.call_count, 2) - expected_msg = 'Synchronizer is _MySynchronizer' - self.assertEqual(mock_logger_info.call_args_list[0][0], (expected_msg, )) - self.assertEqual(mock_logger_info.call_args_list[0][1], {}) - - expected_msg = 'Country is ' + test_country.name - self.assertEqual(mock_logger_info.call_args_list[1][0], (expected_msg, )) - self.assertEqual(mock_logger_info.call_args_list[1][1], {}) - - -class TestVisionDataSynchronizerSync(BaseTenantTestCase): - """Exercise the sync() method of VisionDataSynchronizer class""" - - def _assertVisionSyncLogFundamentals(self, total_records, total_processed, details='', exception_message='', - successful=True): - """Assert common properties of the VisionSyncLog record that should have been created during a test. Populate - the method parameters with what you expect to see in the VisionSyncLog record. - """ - sync_logs = VisionSyncLog.objects.all() - - self.assertEqual(len(sync_logs), 1) - - sync_log = sync_logs[0] - - self.assertEqual(sync_log.country.pk, self.test_country.pk) - self.assertEqual(sync_log.handler_name, '_MySynchronizer') - self.assertEqual(sync_log.total_records, total_records) - self.assertEqual(sync_log.total_processed, total_processed) - self.assertEqual(sync_log.successful, successful) - if details: - self.assertEqual(sync_log.details, details) - else: - self.assertIn(sync_log.details, ('', None)) - if exception_message: - self.assertEqual(sync_log.exception_message, exception_message) - else: - self.assertIn(sync_log.exception_message, ('', None)) - # date_processed is a datetime; there's no way to know the exact microsecond it should contain. As long as - # it's within a few seconds of now, that's good enough. - delta = django_now() - sync_log.date_processed - self.assertLess(delta.seconds, 5) - - def setUp(self): - self.assertEqual(VisionSyncLog.objects.all().count(), 0) - self.test_country = Country.objects.all()[0] - - @mock.patch('etools.applications.vision.vision_data_synchronizer.logger.info') - def test_sync_positive(self, mock_logger_info): - """Test calling sync() for the mainstream case of success. Tests the following -- - - A VisionSyncLog instance is created and has the expected values - - # of records returned by vision can differ from the # returned by synchronizer._convert_records() - - synchronizer._save_records() can return an int (instead of a dict) - - The int returned by synchronizer._save_records() is recorded properly in the VisionSyncLog record - - logger.info() is called as expected - - All calls to synchronizer methods have expected args - """ - synchronizer = _MySynchronizer(country=self.test_country) - - # These are the dummy records that vision will "return" via mock_loader.get() - vision_records = [42, 43, 44] - # These are the dummy records that synchronizer._convert_records() will return. It's intentionally a different - # length than vision_records to test that these two sets of records are treated differently. - converted_records = [42, 44] - - mock_loader = mock.Mock() - mock_loader.url = 'http://example.com' - mock_loader.get.return_value = vision_records - MockLoaderClass = mock.Mock(return_value=mock_loader) - - synchronizer.LOADER_CLASS = MockLoaderClass - - mock_convert_records = mock.Mock(return_value=converted_records) - synchronizer._convert_records = mock_convert_records - - # synchronizer._save_records() should logically return the # of records saved but we're going to make it - # do something different to ensure that its return value is respected. - mock_save_records = mock.Mock(return_value=99) - synchronizer._save_records = mock_save_records - - # Setup is done, now call sync(). - synchronizer.sync() - - self.assertEqual(MockLoaderClass.call_count, 1) - self.assertEqual(MockLoaderClass.call_args[0], tuple()) - self.assertEqual(MockLoaderClass.call_args[1], {'country': self.test_country, - 'endpoint': 'GetSomeStuff_JSON'}) - - self.assertEqual(mock_loader.get.call_count, 1) - self.assertEqual(mock_loader.get.call_args[0], tuple()) - self.assertEqual(mock_loader.get.call_args[1], {}) - - self.assertEqual(mock_convert_records.call_count, 1) - self.assertEqual(mock_convert_records.call_args[0], (vision_records, )) - self.assertEqual(mock_convert_records.call_args[1], {}) - - self.assertEqual(mock_save_records.call_count, 1) - self.assertEqual(mock_save_records.call_args[0], (converted_records, )) - self.assertEqual(mock_save_records.call_args[1], {}) - - # The first two calls to logger.info() are part of the instantiation of VisionDataLoader so I don't need to - # test them here. - self.assertEqual(mock_logger_info.call_count, 4) - expected_msg = '{} records returned from get'.format(len(vision_records)) - self.assertEqual(mock_logger_info.call_args_list[2][0], (expected_msg, )) - self.assertEqual(mock_logger_info.call_args_list[2][1], {}) - expected_msg = '{} records returned from conversion'.format(len(converted_records)) - self.assertEqual(mock_logger_info.call_args_list[3][0], (expected_msg, )) - self.assertEqual(mock_logger_info.call_args_list[3][1], {}) - - self._assertVisionSyncLogFundamentals(len(converted_records), 99) - - def test_sync_save_records_returns_dict(self): - """Test calling sync() when _save_records() returns a dict. Tests that sync() provides default values - as necessary and that values in the dict returned by _save_records() are logged. - """ - synchronizer = _MySynchronizer(country=self.test_country) - - # These are the dummy records that vision will "return" via mock_loader.get() - records = [42, 43, 44] - - mock_loader = mock.Mock() - mock_loader.get.return_value = records - MockLoaderClass = mock.Mock(return_value=mock_loader) - - synchronizer.LOADER_CLASS = MockLoaderClass - - mock_convert_records = mock.Mock(return_value=records) - synchronizer._convert_records = mock_convert_records - - # I'm going to call sync() twice and test a different value from _save_records() each time. - # The first dict is empty to prove that sync() behaves properly even when expected values are missing. - # The second dict contains all expected values, plus an unexpected key/value pair. The extra ensures - # sync() isn't tripped up by that. - save_return_values = [{}, - {'processed': 100, - 'details': 'Hello world!', - 'total_records': 200, - 'foo': 'bar'} - ] - mock_save_records = mock.Mock(side_effect=save_return_values) - synchronizer._save_records = mock_save_records - - # Setup is done, now call sync(). - synchronizer.sync() - - self._assertVisionSyncLogFundamentals(len(records), 0) - - # Get rid of this log record to simplify the remainder of the test. - VisionSyncLog.objects.all()[0].delete() - - # Call sync again. - synchronizer.sync() - - self._assertVisionSyncLogFundamentals(200, 100, details='Hello world!') - - def test_sync_passes_loader_kwargs(self): - """Test that LOADER_EXTRA_KWARGS on the synchronizer are passed to the loader.""" - class _MyFancySynchronizer(_MySynchronizer): - """Synchronizer class that uses LOADER_EXTRA_KWARGS""" - LOADER_EXTRA_KWARGS = ['FROBNICATE', 'POTRZEBIE'] - FROBNICATE = True - POTRZEBIE = 2.2 - - def _convert_records(self, records): - return [] - - def _save_records(self, records): - return 0 - - synchronizer = _MyFancySynchronizer(country=self.test_country) - - mock_loader = mock.Mock() - mock_loader.get.return_value = [42, 43, 44] - MockLoaderClass = mock.Mock(return_value=mock_loader) - - synchronizer.LOADER_CLASS = MockLoaderClass - - # Setup is done, now call sync(). - synchronizer.sync() - - self.assertEqual(MockLoaderClass.call_count, 1) - self.assertEqual(MockLoaderClass.call_args[0], tuple()) - self.assertEqual(MockLoaderClass.call_args[1], {'country': self.test_country, - 'endpoint': 'GetSomeStuff_JSON', - 'FROBNICATE': True, - 'POTRZEBIE': 2.2}) - - @mock.patch('etools.applications.vision.vision_data_synchronizer.logger.info') - def test_sync_exception_handling(self, mock_logger_info): - """Test sync() exception handling behavior.""" - synchronizer = _MySynchronizer(country=self.test_country) - - # Force a failure in the attempt to get vision records - def loader_get_side_effect(): - raise ValueError('Wrong!') - - mock_loader = mock.Mock() - mock_loader.get.side_effect = loader_get_side_effect - MockLoaderClass = mock.Mock(return_value=mock_loader) - - synchronizer.LOADER_CLASS = MockLoaderClass - - # _convert_records() and _save_records() should not be called. I mock them so I can verify that. - mock_convert_records = mock.Mock() - synchronizer._convert_records = mock_convert_records - mock_save_records = mock.Mock() - synchronizer._save_records = mock_save_records - - # Setup is done, now call sync(). - with self.assertRaises(VisionException): - synchronizer.sync() - - self.assertEqual(mock_convert_records.call_count, 0) - self.assertEqual(mock_save_records.call_count, 0) - - # The first two calls to logger.info() are part of the instantiation of VisionDataLoader so I don't need to - # test them here. - self.assertEqual(mock_logger_info.call_count, 3) - expected_msg = 'sync' - self.assertEqual(mock_logger_info.call_args_list[2][0], (expected_msg, )) - self.assertEqual(mock_logger_info.call_args_list[2][1], {'exc_info': True}) - - self._assertVisionSyncLogFundamentals(0, 0, exception_message='Wrong!', successful=False) - - -class TestManualDataLoader(BaseTenantTestCase): - def test_init_no_endpoint_no_object_number(self): - with self.assertRaisesRegexp( - VisionException, - "You must set the ENDPOINT" - ): - ManualDataLoader() - - def test_init_no_endpoint(self): - with self.assertRaisesRegexp( - VisionException, - "You must set the ENDPOINT" - ): - ManualDataLoader(object_number="123") - - def test_init(self): - a = ManualDataLoader(endpoint="api", object_number="123") - self.assertEqual(a.url, "{}/api/123".format(settings.VISION_URL)) diff --git a/src/etools/applications/vision/tests/test_tasks.py b/src/etools/applications/vision/tests/test_tasks.py index ca29093580..0008441877 100644 --- a/src/etools/applications/vision/tests/test_tasks.py +++ b/src/etools/applications/vision/tests/test_tasks.py @@ -7,11 +7,11 @@ from django.utils import timezone import mock +from unicef_vision.exceptions import VisionException import etools.applications.vision.tasks from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.users.tests.factories import CountryFactory -from etools.applications.vision.exceptions import VisionException def _build_country(name): @@ -20,7 +20,9 @@ def _build_country(name): It exists only in memory. We must be careful not to save this because creating a new Country in the database complicates schemas. """ - country = CountryFactory.build(name='Country {}'.format(name.title()), schema_name=name) + country = CountryFactory.build(name='Country {}'.format(name.title()), + schema_name=name, + business_area_code='ZZZ {}'.format(name.title())) country.vision_sync_enabled = True # We'll want to check vision_last_synced as part of the tests, so set it to a known value. country.vision_last_synced = None @@ -120,15 +122,15 @@ def _assertGlobalHandlersSynced(self, mock_handler, all_sync_task=18, public_tas def _assertTenantHandlersSynced(self, mock_handler, all_sync_task=18, sync_t0=6, sync_t1=6, sync_t2=6): """Verify that tenant handler tasks were called all_sync_task is the number of tasks called. - sync_t0 is the number of tasks called for country test 0 - sync_t1 is the number of tasks called for country test 1 - sync_t2 is the number of tasks called for country test 2 + sync_t0 is the number of tasks called for ZZZ Test 0 + sync_t1 is the number of tasks called for ZZZ Test 1 + sync_t2 is the number of tasks called for ZZZ Test 2 """ self.assertEqual(mock_handler.delay.call_count, all_sync_task) countries = [arguments[0][0] for arguments in mock_handler.delay.call_args_list] - self.assertEqual(countries.count('Country Test0'), sync_t0) - self.assertEqual(countries.count('Country Test1'), sync_t1) - self.assertEqual(countries.count('Country Test2'), sync_t2) + self.assertEqual(countries.count('ZZZ Test0'), sync_t0) + self.assertEqual(countries.count('ZZZ Test1'), sync_t1) + self.assertEqual(countries.count('ZZZ Test2'), sync_t2) def _assertLoggerMessages(self, mock_logger, tenant_countries_used=None, selected_synchronizers=None): """Ensure the task sent the appropriate message to Slack. @@ -178,7 +180,7 @@ def test_sync_country_filter_args(self, mock_logger, mock_django_db_connection, selected_countries = [self.tenant_countries[0], ] countryMock.objects.filter = mock.Mock(return_value=selected_countries) mock_django_db_connection.set_tenant = mock.Mock() - etools.applications.vision.tasks.vision_sync_task(country_name='Country Test0') + etools.applications.vision.tasks.vision_sync_task(business_area_code='ZZZ Test0') self._assertCountryMockCalls(countryMock) self._assertGlobalHandlersSynced(mock_handler, all_sync_task=6) @@ -223,7 +225,7 @@ def test_sync_country_and_synchronizer_filter_args(self, mock_logger, mock_djang # Mock connection.set_tenant() so we can verify calls to it. mock_django_db_connection.set_tenant = mock.Mock() etools.applications.vision.tasks.vision_sync_task( - country_name='Country Test0', synchronizers=selected_synchronizers) + business_area_code='ZZZ Test0', synchronizers=selected_synchronizers) self._assertCountryMockCalls(countryMock) self._assertGlobalHandlersSynced(mock_handler, all_sync_task=1, public_task=0) @@ -248,10 +250,10 @@ def test_sync_success(self, Handler, Country, mock_logger_info): """Exercise etools.applications.vision.tasks.sync_handler() success scenario, one matching country.""" Country.objects.get = mock.Mock(return_value=self.country) - etools.applications.vision.tasks.sync_handler.delay(self.country.name, 'programme') - self.assertEqual(mock_logger_info.call_count, 2) - expected_msg = '{} sync successfully for {}'.format( - 'programme', 'Country My' + etools.applications.vision.tasks.sync_handler.delay(self.country.business_area_code, 'programme') + self.assertEqual(mock_logger_info.call_count, 1) + expected_msg = 'Starting vision sync handler {} for country {}'.format( + 'programme', 'ZZZ My' ) self.assertEqual(mock_logger_info.call_args[0], (expected_msg,)) self.assertEqual(mock_logger_info.call_args[1], {}) @@ -265,22 +267,16 @@ def test_sync_vision_error(self, Handler, Country, mock_logger_error, mock_logge """Exercise etools.applications.vision.tasks.sync_handler() which receive an exception from Vision.""" Country.objects.get = mock.Mock(return_value=self.country) - etools.applications.vision.tasks.sync_handler.delay(self.country.name, 'programme') + etools.applications.vision.tasks.sync_handler.delay(self.country.business_area_code, 'programme') # Check that it got retried once - self.assertEqual(mock_logger_info.call_count, 2) - self.assertEqual(mock_logger_error.call_count, 2) + self.assertEqual(mock_logger_info.call_count, 1) + self.assertEqual(mock_logger_error.call_count, 0) expected_msg = 'Starting vision sync handler {} for country {}'.format( - 'programme', 'Country My' + 'programme', 'ZZZ My' ) self.assertEqual(mock_logger_info.call_args[0], (expected_msg,)) self.assertEqual(mock_logger_info.call_args[1], {}) - expected_msg = '{} sync failed, Country: {}'.format( - 'programme', 'Country My' - ) - self.assertEqual(mock_logger_error.call_args[0], (expected_msg,)) - self.assertEqual(mock_logger_error.call_args[1], {'exc_info': True}) - @mock.patch('etools.applications.vision.tasks.logger.error') @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True) def test_sync_country_does_not_exist(self, mock_logger): @@ -289,7 +285,7 @@ def test_sync_country_does_not_exist(self, mock_logger): """ etools.applications.vision.tasks.sync_handler.delay('random', 'programme') self.assertEqual(mock_logger.call_count, 1) - expected_msg = '{} sync failed, Could not find a Country with this name: {}'.format( + expected_msg = '{} sync failed, Could not find a Country with this business area code: {}'.format( 'programme', 'random' ) self.assertEqual(mock_logger.call_args[0], (expected_msg,)) diff --git a/src/etools/applications/vision/tests/test_utils.py b/src/etools/applications/vision/tests/test_utils.py deleted file mode 100644 index 86238fe76d..0000000000 --- a/src/etools/applications/vision/tests/test_utils.py +++ /dev/null @@ -1,66 +0,0 @@ - -import datetime - -from django.test import SimpleTestCase - -from etools.applications.vision import utils - - -class TestWCFJSONDateAsDatetime(SimpleTestCase): - def test_none(self): - self.assertIsNone(utils.wcf_json_date_as_datetime(None)) - - def test_datetime(self): - date = "/Date(1361336400000)/" - result = utils.wcf_json_date_as_datetime(date) - self.assertEqual(result, datetime.datetime(2013, 2, 20, 5, 0)) - - def test_datetime_positive_sign(self): - date = "/Date(00000001+1000)/" - result = utils.wcf_json_date_as_datetime(date) - self.assertEqual( - result, - datetime.datetime(1970, 1, 1, 10, 0, 0, 1000) - ) - - def test_datetime_negative_sign(self): - date = "/Date(00000001-1000)/" - result = utils.wcf_json_date_as_datetime(date) - self.assertEqual( - result, - datetime.datetime(1969, 12, 31, 14, 0, 0, 1000) - ) - - -class TestWCFJSONDateAsDate(SimpleTestCase): - def test_none(self): - self.assertIsNone(utils.wcf_json_date_as_date(None)) - - def test_datetime(self): - date = "/Date(1361336400000)/" - result = utils.wcf_json_date_as_date(date) - self.assertEqual(result, datetime.date(2013, 2, 20)) - - def test_datetime_positive_sign(self): - date = "/Date(00000001+1000)/" - result = utils.wcf_json_date_as_date(date) - self.assertEqual( - result, - datetime.date(1970, 1, 1) - ) - - def test_datetime_negative_sign(self): - date = "/Date(00000001-1000)/" - result = utils.wcf_json_date_as_date(date) - self.assertEqual( - result, - datetime.date(1969, 12, 31) - ) - - -class TestCompDecimals(SimpleTestCase): - def test_not_equal(self): - self.assertFalse(utils.comp_decimals(0.2, 0.3)) - - def test_equal(self): - self.assertTrue(utils.comp_decimals(0.2, 0.2)) diff --git a/src/etools/applications/vision/utils.py b/src/etools/applications/vision/utils.py deleted file mode 100644 index 1ae3fde13a..0000000000 --- a/src/etools/applications/vision/utils.py +++ /dev/null @@ -1,68 +0,0 @@ -import datetime -import json - -from django.conf import settings - -import requests - - -def get_data_from_insight(endpoint, data={}): - url = '{}/{}'.format( - settings.VISION_URL, - endpoint - ).format(**data) - - response = requests.get( - url, - headers={'Content-Type': 'application/json'}, - auth=(settings.VISION_USER, settings.VISION_PASSWORD), - verify=False - ) - if response.status_code != 200: - return False, 'Loading data from Vision Failed, status {}'.format(response.status_code) - try: - result = json.loads(response.json()) - except ValueError: - return False, 'Loading data from Vision Failed, no valid response returned for data: {}'.format(data) - return True, result - - -def wcf_json_date_as_datetime(jd): - if jd is None: - return None - sign = jd[-7] - if sign not in '-+' or len(jd) == 13: - millisecs = int(jd[6:-2]) - else: - millisecs = int(jd[6:-7]) - hh = int(jd[-7:-4]) - mm = int(jd[-4:-2]) - if sign == '-': - mm = -mm - millisecs += (hh * 60 + mm) * 60000 - return datetime.datetime(1970, 1, 1) \ - + datetime.timedelta(microseconds=millisecs * 1000) - - -def wcf_json_date_as_date(jd): - if jd is None: - return None - sign = jd[-7] - if sign not in '-+' or len(jd) == 13: - millisecs = int(jd[6:-2]) - else: - millisecs = int(jd[6:-7]) - hh = int(jd[-7:-4]) - mm = int(jd[-4:-2]) - if sign == '-': - mm = -mm - millisecs += (hh * 60 + mm) * 60000 - my_date = datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds=millisecs * 1000) - return my_date.date() - - -def comp_decimals(y, x): - def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): - return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) - - return isclose(float(x), float(y)) diff --git a/src/etools/applications/vision/vision_data_converter.py b/src/etools/applications/vision/vision_data_converter.py deleted file mode 100644 index da25a62efa..0000000000 --- a/src/etools/applications/vision/vision_data_converter.py +++ /dev/null @@ -1,32 +0,0 @@ -import datetime -import re -from itertools import groupby -from operator import itemgetter - - -def convert_vision_value(key, value): - if isinstance(value, str): - try: - encoded_value = value.encode('ascii', 'ignore') - return int(encoded_value) - except ValueError: - pass - - if isinstance(value, str): - matched_date = re.match(r'/Date\((\d+)\)/', value, re.I) - if matched_date: - return datetime.datetime.fromtimestamp(int(matched_date.group(1)) / 1000.0) - - if key == 'WBS_REFERENCE' and value: - return re.sub(r'(.{4})(.{2})(.{2})', r'\1/\2/\3/', value[0:11]) - - return value - - -def format_records(records, order_info): - def _to_dict(record_order, record_items): - result = dict(zip(order_info, record_order)) - result.update({'ITEMS': list(record_items)}) - return result - - return [_to_dict(order, items) for order, items in groupby(records, key=itemgetter(*order_info))] diff --git a/src/etools/applications/vision/vision_data_synchronizer.py b/src/etools/applications/vision/vision_data_synchronizer.py deleted file mode 100644 index f5aa58c28d..0000000000 --- a/src/etools/applications/vision/vision_data_synchronizer.py +++ /dev/null @@ -1,195 +0,0 @@ -import json -import sys -from abc import ABCMeta, abstractmethod - -from django.conf import settings -from django.db import connection -from django.utils.encoding import force_text - -import requests -from celery.utils.log import get_task_logger - -from etools.applications.vision.exceptions import VisionException -from etools.applications.vision.models import VisionSyncLog - -logger = get_task_logger('vision.synchronize') - -# VISION_NO_DATA_MESSAGE is what the remote vision system returns when it has no data -VISION_NO_DATA_MESSAGE = 'No Data Available' - - -class VisionDataLoader(object): - # Caveat - this loader probably doesn't construct a correct URL when the synchronizer's GLOBAL_CALL = True). - # See https://github.com/unicef/etools/issues/1098 - URL = settings.VISION_URL - - def __init__(self, country=None, endpoint=None): - if endpoint is None: - raise VisionException('You must set the ENDPOINT name') - - separator = '' if self.URL.endswith('/') else '/' - - self.url = '{}{}{}'.format(self.URL, separator, endpoint) - if country and country.name != "Global": - self.url += '/{}'.format(country.business_area_code) - - logger.info('About to get data from {}'.format(self.url)) - - def get(self): - response = requests.get( - self.url, - headers={'Content-Type': 'application/json'}, - auth=(settings.VISION_USER, settings.VISION_PASSWORD), - verify=False - ) - - if response.status_code != 200: - raise VisionException('Load data failed! Http code: {}'.format(response.status_code)) - json_response = response.json() - if json_response == VISION_NO_DATA_MESSAGE: - return [] - - return json_response - - -class FileDataLoader(object): - - def __init__(self, filename=None): - if filename is None: - raise Exception('You need provide the path to the file') - - self.filename = filename - - def get(self): - data = json.load(open(self.filename)) - return data - - -class DataSynchronizer(object): - - __metaclass__ = ABCMeta - - REQUIRED_KEYS = {} - GLOBAL_CALL = False - LOADER_CLASS = None - LOADER_EXTRA_KWARGS = [] - country = None - - @abstractmethod - def _convert_records(self, records): - pass - - @abstractmethod - def _save_records(self, records): - pass - - @abstractmethod - def _get_kwargs(self): - return {} - - def _filter_records(self, records): - def is_valid_record(record): - for key in self.REQUIRED_KEYS: - if key not in record: - return False - return True - - return [rec for rec in records if is_valid_record(rec)] - - def preload(self): - """hook to execute custom code before loading""" - pass - - def sync(self): - """ - Performs the database sync - :return: - """ - log = VisionSyncLog( - country=self.country, - handler_name=self.__class__.__name__ - ) - - self.preload() - loader_kwargs = self._get_kwargs() - loader_kwargs.update({ - kwarg_name: getattr(self, kwarg_name) - for kwarg_name in self.LOADER_EXTRA_KWARGS - }) - data_getter = self.LOADER_CLASS(**loader_kwargs) - - try: - original_records = data_getter.get() - logger.info('{} records returned from get'.format(len(original_records))) - - converted_records = self._convert_records(original_records) - log.total_records = len(converted_records) - logger.info('{} records returned from conversion'.format(len(converted_records))) - - totals = self._save_records(converted_records) - - except Exception as e: - logger.info('sync', exc_info=True) - log.exception_message = force_text(e) - traceback = sys.exc_info()[2] - raise VisionException(force_text(e)).with_traceback(traceback) - else: - if isinstance(totals, dict): - log.total_processed = totals.get('processed', 0) - log.details = totals.get('details', '') - log.total_records = totals.get('total_records', log.total_records) - else: - log.total_processed = totals - log.successful = True - finally: - log.save() - - -class VisionDataSynchronizer(DataSynchronizer): - __metaclass__ = ABCMeta - - ENDPOINT = None - LOADER_CLASS = VisionDataLoader - - def __init__(self, country=None, *args, **kwargs): - if not country: - raise VisionException('Country is required') - if self.ENDPOINT is None: - raise VisionException('You must set the ENDPOINT name') - - logger.info('Synchronizer is {}'.format(self.__class__.__name__)) - - self.country = country - - connection.set_tenant(country) - logger.info('Country is {}'.format(country.name)) - - def _get_kwargs(self): - return { - 'country': self.country, - 'endpoint': self.ENDPOINT, - } - - -class FileDataSynchronizer(DataSynchronizer): - __metaclass__ = ABCMeta - - LOADER_CLASS = FileDataLoader - LOADER_EXTRA_KWARGS = ['filename', ] - - def __init__(self, country=None, *args, **kwargs): - - filename = kwargs.get('filename', None) - if not country: - raise VisionException('Country is required') - if not filename: - raise VisionException('You need provide the path to the file') - - logger.info('Synchronizer is {}'.format(self.__class__.__name__)) - - self.filename = filename - self.country = country - connection.set_tenant(country) - logger.info('Country is {}'.format(country.name)) - - super().__init__(country, *args, **kwargs) diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index 85efe04afb..2945c4189c 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -67,7 +67,7 @@ def get_from_secrets_or_env(var_name, default=None): # DJANGO: CACHE CACHES = { 'default': { - 'BACKEND': 'redis_cache.RedisCache', + 'BACKEND': 'etools.libraries.redis_cache.base.eToolsCache', 'LOCATION': get_from_secrets_or_env('REDIS_URL', 'redis://localhost:6379/0') } } @@ -215,6 +215,7 @@ def get_from_secrets_or_env(var_name, default=None): 'etools.applications.action_points', 'unicef_snapshot', 'unicef_attachments', + 'unicef_vision', ) INSTALLED_APPS = ('django_tenants',) + SHARED_APPS + TENANT_APPS @@ -463,6 +464,7 @@ def before_send(event, hint): TASK_ADMIN_USER = get_from_secrets_or_env('TASK_ADMIN_USER', 'etools_task_admin') +VISION_LOGGER_MODEL = "vision.VisionSyncLog" VISION_URL = get_from_secrets_or_env('VISION_URL', 'http://invalid_vision_url') VISION_USER = get_from_secrets_or_env('VISION_USER', 'invalid_vision_user') VISION_PASSWORD = get_from_secrets_or_env('VISION_PASSWORD', 'invalid_vision_password') diff --git a/src/etools/libraries/azure_graph_api/tasks.py b/src/etools/libraries/azure_graph_api/tasks.py index a3bb401d11..b9218492ef 100644 --- a/src/etools/libraries/azure_graph_api/tasks.py +++ b/src/etools/libraries/azure_graph_api/tasks.py @@ -4,9 +4,9 @@ import requests from celery.utils.log import get_task_logger +from unicef_vision.exceptions import VisionException from etools.applications.users.models import Country -from etools.applications.vision.exceptions import VisionException from etools.applications.vision.models import VisionSyncLog from etools.config.celery import app from etools.libraries.azure_graph_api.client import azure_sync_users, get_token diff --git a/src/etools/libraries/djangolib/admin.py b/src/etools/libraries/djangolib/admin.py new file mode 100644 index 0000000000..3dfcc145ae --- /dev/null +++ b/src/etools/libraries/djangolib/admin.py @@ -0,0 +1,30 @@ +from django.db.models import ForeignKey, ManyToManyField, OneToOneField + +from etools.libraries.djangolib.models import EPOCH_ZERO + + +class AdminListMixin: + exclude_fields = ['id', 'deleted_at'] + custom_fields = ['is_deleted'] + + def __init__(self, model, admin_site): + """ + Gather all the fields from model meta expect fields in 'exclude_fields'. + Add all fields listed in 'custom_fields'. Those can be admin method, model methods etc. + """ + self.list_display = [field.name for field in model._meta.fields if field.name not in self.exclude_fields] + self.list_display += self.custom_fields + self.search_fields = [field.name for field in model._meta.fields if isinstance( + not field, (OneToOneField, ForeignKey, ManyToManyField)) and field.name not in self.exclude_fields] + + super().__init__(model, admin_site) + + def is_deleted(self, obj): + """ + Display a deleted indicator on the admin page (green/red tick). + """ + if hasattr(obj, "deleted_at"): + return obj.deleted_at == EPOCH_ZERO + + is_deleted.short_description = 'Deleted' + is_deleted.boolean = True diff --git a/src/etools/libraries/djangolib/models.py b/src/etools/libraries/djangolib/models.py index 42539e1168..b3a17ea180 100644 --- a/src/etools/libraries/djangolib/models.py +++ b/src/etools/libraries/djangolib/models.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import Group from django.db import models +from django.db.models import Aggregate from django.db.models.query import QuerySet from django.db.models.query_utils import Q from django.utils.timezone import now @@ -68,7 +69,7 @@ def invalidate_instances(cls): class StringConcat(models.Aggregate): """ A custom aggregation function that returns "," separated strings """ - + allow_distinct = True function = 'GROUP_CONCAT' template = '%(function)s(%(distinct)s%(expressions)s)' @@ -86,7 +87,11 @@ def as_postgresql(self, compiler, connection): return super().as_sql(compiler, connection) -class DSum(models.Aggregate): +class MaxDistinct(models.Max): + allow_distinct = True + + +class DSum(Aggregate): function = 'SUM' template = '%(function)s(DISTINCT %(expressions)s)' name = 'Sum' diff --git a/src/etools/libraries/djangolib/urlresolvers.py b/src/etools/libraries/djangolib/urlresolvers.py deleted file mode 100644 index f00453035b..0000000000 --- a/src/etools/libraries/djangolib/urlresolvers.py +++ /dev/null @@ -1,10 +0,0 @@ -from urllib.parse import urljoin - -from django.conf import settings - - -def build_absolute_url(url): - if not url: - return '' - - return urljoin(settings.HOST, url) diff --git a/src/etools/applications/audit/purchase_order/utils.py b/src/etools/libraries/redis_cache/__init__.py similarity index 100% rename from src/etools/applications/audit/purchase_order/utils.py rename to src/etools/libraries/redis_cache/__init__.py diff --git a/src/etools/libraries/redis_cache/base.py b/src/etools/libraries/redis_cache/base.py new file mode 100644 index 0000000000..7a5dd90cf1 --- /dev/null +++ b/src/etools/libraries/redis_cache/base.py @@ -0,0 +1,95 @@ +from redis.exceptions import ConnectionError +from redis_cache import RedisCache +from redis_cache.backends.base import DEFAULT_TIMEOUT + + +class eToolsCache(RedisCache): + def _get(self, client, key, default=None): + try: + value = super()._get(client, key, default) + except ConnectionError: + value = default + return value + + def _set(self, client, key, value, timeout, _add_only=False): + try: + result = super()._set(client, key, value, timeout, _add_only) + except ConnectionError: + result = value + return result + + def _delete_many(self, client, keys): + try: + super()._delete_many(client, keys) + except ConnectionError: + pass + + def _clear(self, client): + try: + super()._clear(client) + except ConnectionError: + pass + + def _get_many(self, client, original_keys, versioned_keys): + try: + data = super()._get_many(client, original_keys, versioned_keys) + except ConnectionError: + data = {} + return data + + def _incr_version(self, client, old, new, original, delta, version): + try: + result = super()._incr_version( + client, + old, + new, + original, + delta, + version, + ) + except ConnectionError: + result = None + return result + + def _delete_pattern(self, client, pattern): + try: + super()._delete_pattern(client, pattern) + except ConnectionError: + pass + + def get_or_set( + self, + key, + func, + timeout=DEFAULT_TIMEOUT, + lock_timeout=None, + stale_cache_timeout=None, + ): + try: + value = super().get_or_set( + key, + func, + timeout=timeout, + lock_timeout=lock_timeout, + stale_cache_timeout=stale_cache_timeout, + ) + except ConnectionError: + try: + value = func() + except Exception: + raise + return value + + def incr(self, key, delta=1): + try: + value = super().incr(key, delta=delta) + except ConnectionError: + value = delta + return value + + def delete(self, key): + try: + result = super().delete(key) + except ConnectionError: + result = True + return result diff --git a/src/etools/templates/404.html b/src/etools/templates/404.html index 828d8e99e1..b13bb1592f 100644 --- a/src/etools/templates/404.html +++ b/src/etools/templates/404.html @@ -1,25 +1,26 @@ -{% extends "base_admin.html" %} +{% extends "login_base.html" %} -{% block body %} +{% block content %} +
    -

    404

    - -

    Page not found

    +

    404 - Page Not Found

    We're having trouble loading the page you are looking for.

    {% for message in messages %} -

    {{ message }}

    +

    {{ message }}

    {% endfor %}
    +
    +
    @@ -27,4 +28,5 @@

    404

    +
    {% endblock %} diff --git a/src/etools/templates/500.html b/src/etools/templates/500.html index b94ef60cbe..b8ee822489 100644 --- a/src/etools/templates/500.html +++ b/src/etools/templates/500.html @@ -1,14 +1,14 @@ -{% extends "base_admin.html" %} +{% extends "login_base.html" %} -{% block body %} + +{% block content %} +
    -

    500

    - -

    Something went wrong.

    +

    500 - Something went wrong

    Our engineers have have been notified and will address the issue shortly

    @@ -17,6 +17,8 @@

    500

    +
    +
    @@ -24,5 +26,6 @@

    500

    -{% endblock %} +
    +{% endblock content %} diff --git a/src/etools/templates/admin/base_site.html b/src/etools/templates/admin/base_site.html index c05342c1a4..7d7a2d21a1 100644 --- a/src/etools/templates/admin/base_site.html +++ b/src/etools/templates/admin/base_site.html @@ -61,6 +61,9 @@ float: right; } + .module caption.tenant-app { + background: #f7d904; + } /* Footer */ #footer @@ -124,9 +127,11 @@ var host = { 'localhost' : 'Local Testing Environment', 'etools-dev.unicef.org' : 'Develop Testing Environment', - 'etools-staging.unicef.org': 'Staging Testing Environment' + 'etools-staging.unicef.org': 'Staging Testing Environment', + 'etools-demo.unicef.org' : 'Demo Testing Environment', + 'etools-test.unicef.org' : 'Engineering Testing Environment' }; - var cond = /localhost|etools-dev|etools-staging/g; + var cond = /localhost|etools-dev|etools-staging|etools-demo|etools-test/g; if (cond.test(window.location)) { $('#header').css({'background': '#BE1A1A'}); $('#test-text').show().text(' ' + host[window.location.hostname]); diff --git a/src/etools/templates/admin/index.html b/src/etools/templates/admin/index.html deleted file mode 100644 index dbd91d9539..0000000000 --- a/src/etools/templates/admin/index.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_static etools %} - -{% block extrastyle %}{{ block.super }}{% endblock %} - -{% block coltype %}colMS{% endblock %} - -{% block bodyclass %}{{ block.super }} dashboard{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} -
    - -{% if app_list %} - {% for app in app_list %} - {% tenant_model_filter app as tenant_app %} - {% tenant_app_filter app as tenant_app_filter %} -
    -
    Tasks
    Sync All Users
    Sync Delta Users
    Update Hact Values
    Aggregate Hact for Charts
    - {% if tenant_app %} - - {% for model in app.models %} - - {% if model.admin_url %} - - {% else %} - - {% endif %} - - {% if model.add_url %} - - {% else %} - - {% endif %} - - {% if model.admin_url %} - - {% else %} - - {% endif %} - - {% endfor %} - {% else %} - - - - - {% endif %} -
    - {{ app.name }} -
    {{ model.name }}{{ model.name }}{% trans 'Add' %} {% trans 'Change' %} 
    - {{ app.name }} -
    {% trans "Not available for Global schema" %}
    - - {% endfor %} -{% else %} -

    {% trans "You don't have permission to edit anything." %}

    -{% endif %} - -{% endblock %} - -{% block sidebar %} - -{% endblock %} diff --git a/src/etools/templates/login_base.html b/src/etools/templates/login_base.html index d88da6c9d2..e7171bc14c 100644 --- a/src/etools/templates/login_base.html +++ b/src/etools/templates/login_base.html @@ -3,69 +3,73 @@ - - - - - - UNICEF eTools - - - - -{% block extra_head %} - - - - - {% endblock %} + + + + + + UNICEF eTools + + + + + {% block extra_head %} + + + + + {% endblock %} - {% block toolbar %} - {% endblock toolbar %} - - - {% block content %} - {% endblock content %} - {% block extra_js %} - {% endblock %} +{% block toolbar %} +
    + + +
    +{% endblock toolbar %} + + +{% block content %} +{% endblock content %} +{% block extra_js %} +{% endblock %} diff --git a/src/etools/templates/no_country_found.html b/src/etools/templates/no_country_found.html index b17fae1c10..91488fada1 100644 --- a/src/etools/templates/no_country_found.html +++ b/src/etools/templates/no_country_found.html @@ -1,23 +1,24 @@ {% extends "login_base.html" %} -{% block toolbar %} -
    - -
    Country workspace not found on user
    -
    -{% endblock toolbar %} - {% block content %} -
    -

    Hi {{ user }},

    -

    - Your user profile does not have a country workspace associated with it. -
    - Please contact the eTools focal point within your organization to help you gain access. -

    - Go to Staging environment -
    -

    Please Log Out to reflect any updates in the future

    -
    +
    + +
    +

    Country workspace not found on user

    +
    + +

    Hi {{ user }},

    +

    + +

    Country workspace not found on user
    + + Your user profile does not have a country workspace associated with it. +
    + Please contact the eTools focal point within your organization to help you gain access. +

    + Go to Staging environment +
    +

    Please Log Out to reflect any updates in the future

    +
    {% endblock content %} diff --git a/src/etools/templates/removed_workspace.html b/src/etools/templates/removed_workspace.html index d6f7c1f235..2d2c96b627 100644 --- a/src/etools/templates/removed_workspace.html +++ b/src/etools/templates/removed_workspace.html @@ -1,21 +1,20 @@ {% extends "login_base.html" %} -{% block toolbar %} -
    - -
    -{% endblock toolbar %} - {% block content %} -
    -

    Hi {{ user }},

    -

    - Your workspace is currently inactive -
    - Please contact the eTools focal point within your organization with any questions. -

    - Go to Staging environment +
    + +
    +

    Workspace not found

    +
    + +

    Hi {{ user }},

    +

    + Your workspace is currently inactive
    -

    Log Out

    -
    + Please contact the eTools focal point within your organization with any questions. +

    + Go to Staging environment +
    +

    Log Out

    +
    {% endblock content %} \ No newline at end of file diff --git a/tox.ini b/tox.ini index 4791e056cd..c92fd4b844 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = d{20,21} +envlist = d{21,22} [testenv] basepython=python3.6 @@ -17,16 +17,16 @@ whitelist_externals = pipenv commands = pipenv install --dev --ignore-pipfile -[testenv:d20] +[testenv:d21] commands = {[testenv]commands} - pip install "django>2.0,<2.1" + pip install "django>=2.1,<2.2" ./runtests.sh -[testenv:d21] +[testenv:d22] commands = {[testenv]commands} - pip install "django>2.1,<2.2" + pip install "django>=2.2,<2.3" ./runtests.sh [testenv:report]