From 661b57f4aa80e006d2aacf8d722e3e262e8832ea Mon Sep 17 00:00:00 2001 From: denes csaba Date: Wed, 3 Jul 2019 19:25:30 +0300 Subject: [PATCH 01/56] Return validationerror with status 400 when trying to add duplicate ssfa interventions to an agreement --- .../applications/partners/views/interventions_v2.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index fe012fec8f..df89c59e41 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -31,6 +31,7 @@ PartnerScopeFilter, ) from etools.applications.partners.models import ( + Agreement, Intervention, InterventionAmendment, InterventionAttachment, @@ -146,6 +147,15 @@ def create(self, request, *args, **kwargs): 'result_links' ] nested_related_names = ['ll_results'] + + if request.data.get('document_type') == Intervention.SSFA: + agreement = Agreement.objects.get(pk=request.data.get('agreement')) + if agreement and agreement.interventions.all().count(): + raise ValidationError( + 'You can only add one SSFA Document for each SSFA Agreement', + status.HTTP_400_BAD_REQUEST + ) + serializer = self.my_create(request, related_fields, nested_related_names=nested_related_names, From 194f85ef2dff3e65037364a2afd6549427d7ebf5 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Wed, 3 Jul 2019 20:03:59 +0300 Subject: [PATCH 02/56] add test --- .../applications/partners/tests/test_views.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/etools/applications/partners/tests/test_views.py b/src/etools/applications/partners/tests/test_views.py index 7aa72326fc..894531a2c5 100644 --- a/src/etools/applications/partners/tests/test_views.py +++ b/src/etools/applications/partners/tests/test_views.py @@ -1420,6 +1420,31 @@ def test_intervention_validation_doctype_ssfa(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn('Document type PD or HPD can only be associated with a PCA agreement.', response.data) + def test_intervention_validation_multiple_agreement_ssfa(self): + self.agreement.agreement_type = Agreement.SSFA + self.agreement.save() + # self.agreement. + intervention = Intervention.objects.get(id=self.intervention["id"]) + intervention.document_type = Intervention.SSFA + intervention.save() + self.agreement.interventions.add(intervention) + + response = self.forced_auth_req( + 'post', + reverse( + "partners_api:intervention-list" + ), + user=self.partnership_manager_user, + data={ + "agreement": self.agreement.id, + "document_type": Intervention.SSFA, + "status": Intervention.DRAFT, + "title": "test" + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('You can only add one SSFA Document for each SSFA Agreement', response.data) + def test_intervention_validation_dates(self): today = datetime.date.today() data = { From 1a3caead6c719503f261c6a069d765111d38656f Mon Sep 17 00:00:00 2001 From: denes csaba Date: Thu, 4 Jul 2019 13:59:54 +0300 Subject: [PATCH 03/56] cleanup & fixes --- src/etools/applications/partners/tests/test_views.py | 1 - src/etools/applications/partners/validation/interventions.py | 2 +- src/etools/applications/partners/views/interventions_v2.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/etools/applications/partners/tests/test_views.py b/src/etools/applications/partners/tests/test_views.py index 894531a2c5..fc50e73ff8 100644 --- a/src/etools/applications/partners/tests/test_views.py +++ b/src/etools/applications/partners/tests/test_views.py @@ -1423,7 +1423,6 @@ def test_intervention_validation_doctype_ssfa(self): def test_intervention_validation_multiple_agreement_ssfa(self): self.agreement.agreement_type = Agreement.SSFA self.agreement.save() - # self.agreement. intervention = Intervention.objects.get(id=self.intervention["id"]) intervention.document_type = Intervention.SSFA intervention.save() diff --git a/src/etools/applications/partners/validation/interventions.py b/src/etools/applications/partners/validation/interventions.py index fda53d784c..a2ac508335 100644 --- a/src/etools/applications/partners/validation/interventions.py +++ b/src/etools/applications/partners/validation/interventions.py @@ -222,7 +222,7 @@ def ssfa_agreement_has_no_other_intervention(i): if i.document_type == i.SSFA: if not(i.agreement.agreement_type == i.agreement.SSFA): raise BasicValidationError(_('Agreement selected is not of type SSFA')) - return i.agreement.interventions.all().count() <= 1 + return i.agreement.interventions.count() <= 1 return True diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index df89c59e41..fd7148e212 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -150,7 +150,7 @@ def create(self, request, *args, **kwargs): if request.data.get('document_type') == Intervention.SSFA: agreement = Agreement.objects.get(pk=request.data.get('agreement')) - if agreement and agreement.interventions.all().count(): + if agreement and agreement.interventions.count(): raise ValidationError( 'You can only add one SSFA Document for each SSFA Agreement', status.HTTP_400_BAD_REQUEST From 27322bd201f13194cb070048f3f703470d916f79 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 22 Jul 2019 14:03:31 -0400 Subject: [PATCH 04/56] update reqs --- Pipfile | 12 +++--- Pipfile.lock | 114 ++++++++++++++++++++++++--------------------------- 2 files changed, 59 insertions(+), 67 deletions(-) diff --git a/Pipfile b/Pipfile index eb950d28f5..eaf8b68334 100644 --- a/Pipfile +++ b/Pipfile @@ -30,17 +30,17 @@ django-celery-email = "==2.0.2" django_celery_results = "==1.1.2" django-contrib-comments = "==1.9.1" django-cors-headers = "==3.0.2" -django-debug-toolbar = "==1.11" -django-extensions = "==2.0" +django-debug-toolbar = "==2.0" +django-extensions = "==2.2.1" django-easy-pdf = "==0.1.1" -django-filter = "==2.1" +django-filter = "==2.2" 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-ordered-model = "==3.1.1" +django-ordered-model = "==3.3" django-post_office = "==3.2.1" django-redis-cache = "==2.0" django-rest-swagger = "==2.2" @@ -53,7 +53,7 @@ djangorestframework-gis = "==0.14" djangorestframework-jwt = "==1.11.0" djangorestframework-recursive = "==0.1.2" djangorestframework-xml = "==1.4" -djangorestframework = "==3.9.4" +djangorestframework = "==3.10.1" drf-nested-routers = "==0.91" drf-querystringfilter = "==1.0.0" etools-validator = "==0.3.2" @@ -63,7 +63,7 @@ gunicorn = "==19.9" newrelic = "==4.20.1.121" Pillow = "==5.4.1" psycopg2-binary = "==2.8.3" -sentry-sdk = "==0.9.5" +sentry-sdk = "==0.10.2" requests = "==2.22" social-auth-app-django = "==3.1" social-auth-core = {extras = ["azuread"],version = "==3.2"} diff --git a/Pipfile.lock b/Pipfile.lock index 5d64ed3027..c107315693 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "3af1c5bbab618934db17912bab1694a7379b7a20a65e2fc72c4842e5f6afa991" + "sha256": "23ab3bf6e0fd9bee31c6086127cd44227aea5c83d85cee73f283bb48537f223a" }, "pipfile-spec": 6, "requires": { - "python_version": "3.6" + "python_version": "3.7" }, "sources": [ { @@ -778,9 +778,9 @@ }, "django-autocomplete-light": { "hashes": [ - "sha256:69a34f97a5b8ab3aac6baaea36f627ef09eaa5791c60f881b2000b1b813268d0" + "sha256:29ce2626a11eab2333e5aa9f95166a6d4400f11b5a05e8f23fa77017b1a9089a" ], - "version": "==3.3.5" + "version": "==3.4.1" }, "django-celery-beat": { "hashes": [ @@ -824,11 +824,11 @@ }, "django-debug-toolbar": { "hashes": [ - "sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736", - "sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0" + "sha256:17c53cd6bf4e7d69902aedf9a1d26c5d3b7369b54c5718744704f27b5a72f35d", + "sha256:9a23ada2e43cd989195db3c18710b5d7451134a0d48127ab64c1d2ad81700342" ], "index": "pypi", - "version": "==1.11" + "version": "==2.0" }, "django-easy-pdf": { "hashes": [ @@ -839,19 +839,19 @@ }, "django-extensions": { "hashes": [ - "sha256:308e4aa61b6accc249c67a0fb99daef6f8b233179dd50a52f408fae4f58b71ee", - "sha256:7c0db3b1a249bcfdd50c4314a617f8fb0aec979e2c01466e68ec0d60337313a9" + "sha256:4aafdb865104eaa5d681b9976b36c52c9d441be89b7d782e40808f1c5c0c8f93", + "sha256:8a2552fdeb222b23895ef52cdc28fc56efba976f6da07ca92937f6f5e626e345" ], "index": "pypi", - "version": "==2.0" + "version": "==2.2.1" }, "django-filter": { "hashes": [ - "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", - "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" + "sha256:558c727bce3ffa89c4a7a0b13bc8976745d63e5fd576b3a9a851650ef11c401b", + "sha256:c3deb57f0dd7ff94d7dce52a047516822013e2b441bed472b722a317658cfd14" ], "index": "pypi", - "version": "==2.1" + "version": "==2.2" }, "django-fsm": { "hashes": [ @@ -908,10 +908,11 @@ }, "django-ordered-model": { "hashes": [ - "sha256:14aeacca5a4d41de92b89432a7665b6be5ef254e72418cbeb32258348ad05352" + "sha256:0931f498008f91a00a32c4e0ae08a662ef608a1092bf6e6ec9af9b1a83f08acf", + "sha256:abf0d963f7e607a994baf6bc300e50af647b3d243c3e592c6cc8f8b924b6d427" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.3" }, "django-post-office": { "hashes": [ @@ -971,11 +972,11 @@ }, "djangorestframework": { "hashes": [ - "sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651", - "sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb" + "sha256:1ca4a5599a5ec31f3d6238a687fcc1dd4c41b1d90edab9ad398fcbf87872b7ba", + "sha256:c3c5edfdbc5dd33f9121bb84305bfd603d2c791f20cff9782772f44a7684a4e4" ], "index": "pypi", - "version": "==3.9.4" + "version": "==3.10.1" }, "djangorestframework-csv": { "hashes": [ @@ -1342,15 +1343,16 @@ }, "pyrestcli": { "hashes": [ - "sha256:ec07eb04a6a0088a88719c8d9acf47e51095fa4079cfee7a95dbd23d4b859176" + "sha256:67c37fe8d1d01ecd8fa213bca8414dbb4b9a065f0c0999ad93c02e908475593b", + "sha256:ea159468e689ecccb1c2b4fc42237ff073edb0a36def89a7c408c4130c6c0c69" ], - "version": "==0.6.11" + "version": "==0.6.12" }, "python-crontab": { "hashes": [ - "sha256:21bf01edd59a7357cdc9b1d911b16430499bf51172dd8c72d0c985c652f14057" + "sha256:11488bbac026bf77a659fee0bf8e20de9651753ad0fdf6649237e3b9e04b866a" ], - "version": "==2.3.7" + "version": "==2.3.8" }, "python-dateutil": { "hashes": [ @@ -1460,11 +1462,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:692aa93637273365166041ee8e06ccd6b61d5f06560765d78828edbaac83fae1", - "sha256:7c9db0e419fb0fb31c1b1d2ec9247667d2b77bd4f3136119ee6f1464e9b088a4" + "sha256:d491aa6399eaa3eded433972751a9770180730fd8b4c225b0b7f49c4fa2af70b", + "sha256:d68003cdffbbfcadaa2c445b72e1050b0a44406f94199866f192986c016b23f5" ], "index": "pypi", - "version": "==0.9.5" + "version": "==0.10.2" }, "simplejson": { "hashes": [ @@ -1550,14 +1552,6 @@ ], "version": "==5.1.1" }, - "typing": { - "hashes": [ - "sha256:38566c558a0a94d6531012c8e917b1b8518a41e418f7f15f00e129cc80162ad3", - "sha256:53765ec4f83a2b720214727e319607879fec4acde22c4fbb54fa2604e79e44ce", - "sha256:84698954b4e6719e912ef9a42a2431407fe3755590831699debda6fba92aac55" - ], - "version": "==3.7.4" - }, "unicef-attachments": { "hashes": [ "sha256:6d6bd2e606caf06d2b78762ab5319e6f39d569f123e665473e23a874669b8a7f" @@ -1755,19 +1749,17 @@ }, "djangorestframework": { "hashes": [ - "sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651", - "sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb" + "sha256:1ca4a5599a5ec31f3d6238a687fcc1dd4c41b1d90edab9ad398fcbf87872b7ba", + "sha256:c3c5edfdbc5dd33f9121bb84305bfd603d2c791f20cff9782772f44a7684a4e4" ], "index": "pypi", - "version": "==3.9.4" + "version": "==3.10.1" }, "docutils": { "hashes": [ - "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", - "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", - "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521" ], - "version": "==0.14" + "version": "==0.15.post1" }, "drf-api-checker": { "hashes": [ @@ -1793,10 +1785,10 @@ }, "faker": { "hashes": [ - "sha256:1c0a5e7bb54d2c54569986a27124715c83899e592d8d61d4e372dbff6c699573", - "sha256:60477f757a80f665bbe1fb3d1cfe5d205ec7b99d5240114de7b27b4c25d236ca" + "sha256:96ad7902706f2409a2d0c3de5132f69b413555a419bacec99d3f16e657895b47", + "sha256:b3bb64aff9571510de6812df45122b633dbc6227e870edae3ed9430f94698521" ], - "version": "==1.0.7" + "version": "==2.0.0" }, "fancycompleter": { "hashes": [ @@ -1813,11 +1805,11 @@ }, "flake8": { "hashes": [ - "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", - "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", + "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" ], "index": "pypi", - "version": "==3.7.7" + "version": "==3.7.8" }, "freezegun": { "hashes": [ @@ -1873,10 +1865,10 @@ }, "jedi": { "hashes": [ - "sha256:49ccb782651bb6f7009810d17a3316f8867dde31654c750506970742e18b553d", - "sha256:79d0f6595f3846dffcbe667cc6dc821b96e5baa8add125176c31a3917eb19d58" + "sha256:53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", + "sha256:e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c" ], - "version": "==0.14.0" + "version": "==0.14.1" }, "jinja2": { "hashes": [ @@ -1976,10 +1968,10 @@ }, "parso": { "hashes": [ - "sha256:5052bb33be034cba784193e74b1cde6ebf29ae8b8c1e4ad94df0c4209bfc4826", - "sha256:db5881df1643bf3e66c097bfd8935cf03eae73f4cb61ae4433c9ea4fb6613446" + "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", + "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c" ], - "version": "==0.5.0" + "version": "==0.5.1" }, "pdbpp": { "hashes": [ @@ -2055,10 +2047,10 @@ }, "pyparsing": { "hashes": [ - "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", - "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + "sha256:530d8bf8cc93a34019d08142593cf4d78a05c890da8cf87ffa3120af53772238", + "sha256:f78e99616b6f1a4745c0580e170251ef1bbafc0d0513e270c4bd281bf29d2800" ], - "version": "==2.4.0" + "version": "==2.4.1" }, "python-dateutil": { "hashes": [ @@ -2215,10 +2207,10 @@ }, "virtualenv": { "hashes": [ - "sha256:b7335cddd9260a3dd214b73a2521ffc09647bde3e9457fcca31dc3be3999d04a", - "sha256:d28ca64c0f3f125f59cabf13e0a150e1c68e5eea60983cc4395d88c584495783" + "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19", + "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52" ], - "version": "==16.6.1" + "version": "==16.6.2" }, "wcwidth": { "hashes": [ @@ -2258,10 +2250,10 @@ }, "zipp": { "hashes": [ - "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d", - "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3" + "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", + "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" ], - "version": "==0.5.1" + "version": "==0.5.2" } } } From 31c1caef6389013f14bed2ecaaf15fa459112ea6 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 22 Jul 2019 15:52:49 -0400 Subject: [PATCH 05/56] fix tests --- .../tests/vcr_cassettes/fund_reservation.yml | 83 ++++++------------- .../applications/t2f/serializers/travel.py | 4 +- 2 files changed, 29 insertions(+), 58 deletions(-) diff --git a/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation.yml b/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation.yml index 591db14fc9..9cdc2f6b8d 100644 --- a/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation.yml +++ b/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation.yml @@ -11,69 +11,40 @@ interactions: Content-Type: - application/json User-Agent: - - python-requests/2.21.0 + - python-requests/2.22.0 method: GET uri: http://invalid_vision_url/GetFundsReservationInfo_JSON/9999 response: body: - string: '"{ \"ROWSET\": { \"ROW\": [ {\"ACTUAL_CASH_TRANSFER\": \"50197.02\", - \"ACTUAL_CASH_TRANSFER_DC\": \"2689656.62\", \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": - \"PHP\", \"CURRENT_FR_AMOUNT\": \"50197.02\", \"DONOR_CODE\": \"CODE X\", - \"DONOR_NAME\": \"DONOR X\", \"DUE_DATE\": \"04-FEB-19\", \"FR_DOC_DATE\": - \"17-OCT-18\", \"FR_DOCUMENT_TEXT\": \"FR FOR MRM PD NP\", \"FR_END_DATE\": - \"31-MAR-19\", \"FR_LINE_ITEM_TEXT\": \"FR FOR NP\", \"FR_NUMBER\": \"9999\", - \"FR_OVERALL_AMOUNT\": \"179643.93\", \"FR_START_DATE\": \"17-OCT-18\", \"FR_TYPE\": - \"Programme Document Against PCA\", \"FUND\": \"SC\", \"GRANT_NBR\": \"GRANT W\", - \"LINE_ITEM\": \"4\", \"MULTI_CURR_FLAG\": \"N\", \"OUTSTANDING_DCT\": \"0\", - \"OUTSTANDING_DCT_DC\": \"0\", \"OVERALL_AMOUNT\": \"26425.53\", \"OVERALL_AMOUNT_DC\": - \"1430942.38\", \"VENDOR_CODE\": \"PVN\", \"WBS_ELEMENT\": \"9999\\\/A0\\\/06\\\/003\\\/006\\\/005\" - }, {\"ACTUAL_CASH_TRANSFER\": \"50197.02\", \"ACTUAL_CASH_TRANSFER_DC\": \"2689656.62\", - \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": \"PHP\", \"CURRENT_FR_AMOUNT\": \"50197.02\", - \"DONOR_CODE\": \"CODE X\", \"DONOR_NAME\": \"DONOR X\", \"DUE_DATE\": - \"17-OCT-18\", \"FR_DOC_DATE\": \"17-OCT-18\", \"FR_DOCUMENT_TEXT\": \"FR - FOR MRM PD NP\", \"FR_END_DATE\": \"31-MAR-19\", \"FR_LINE_ITEM_TEXT\": \"FR - FOR NP\", \"FR_NUMBER\": \"9999\", \"FR_OVERALL_AMOUNT\": \"179643.93\", - \"FR_START_DATE\": \"17-OCT-18\", \"FR_TYPE\": \"Programme Document Against - PCA\", \"FUND\": \"SC\", \"GRANT_NBR\": \"GRANT W\", \"LINE_ITEM\": \"2\", - \"MULTI_CURR_FLAG\": \"N\", \"OUTSTANDING_DCT\": \"0\", \"OUTSTANDING_DCT_DC\": - \"0\", \"OVERALL_AMOUNT\": \"30081.45\", \"OVERALL_AMOUNT_DC\": \"1628910.25\", - \"VENDOR_CODE\": \"PVN\", \"WBS_ELEMENT\": \"9999\\\/A0\\\/05\\\/803\\\/005\\\/004\" - }, {\"ACTUAL_CASH_TRANSFER\": \"50197.02\", \"ACTUAL_CASH_TRANSFER_DC\": \"2689656.62\", - \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": \"PHP\", \"CURRENT_FR_AMOUNT\": \"50197.02\", - \"DONOR_CODE\": \"CODE X\", \"DONOR_NAME\": \"DONOR Y\", \"DUE_DATE\": \"04-FEB-19\", - \"FR_DOC_DATE\": \"17-OCT-18\", \"FR_DOCUMENT_TEXT\": \"FR FOR MRM PD NP\", - \"FR_END_DATE\": \"31-MAR-19\", \"FR_LINE_ITEM_TEXT\": \"FR FOR NP\", \"FR_NUMBER\": - \"9999\", \"FR_OVERALL_AMOUNT\": \"179643.93\", \"FR_START_DATE\": \"17-OCT-18\", - \"FR_TYPE\": \"Programme Document Against PCA\", \"FUND\": \"SC\", \"GRANT_NBR\": - \"GRANT W\", \"LINE_ITEM\": \"5\", \"MULTI_CURR_FLAG\": \"N\", \"OUTSTANDING_DCT\": - \"0\", \"OUTSTANDING_DCT_DC\": \"0\", \"OVERALL_AMOUNT\": \"48014.77\", \"OVERALL_AMOUNT_DC\": - \"2600000\", \"VENDOR_CODE\": \"PVN\", \"WBS_ELEMENT\": \"9999\\\/A0\\\/06\\\/003\\\/006\\\/005\" - }, {\"ACTUAL_CASH_TRANSFER\": \"50197.02\", \"ACTUAL_CASH_TRANSFER_DC\": \"2689656.62\", - \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": \"PHP\", \"CURRENT_FR_AMOUNT\": \"50197.02\", - \"DONOR_CODE\": \"N\\\/A\", \"DONOR_NAME\": \"N\\\/A\", \"DUE_DATE\": \"17-OCT-18\", - \"FR_DOC_DATE\": \"17-OCT-18\", \"FR_DOCUMENT_TEXT\": \"FR FOR MRM PD NP\", - \"FR_END_DATE\": \"31-MAR-19\", \"FR_LINE_ITEM_TEXT\": \"FR FOR NP\", \"FR_NUMBER\": - \"9999\", \"FR_OVERALL_AMOUNT\": \"179643.93\", \"FR_START_DATE\": \"17-OCT-18\", - \"FR_TYPE\": \"Programme Document Against PCA\", \"FUND\": \"GC\", \"GRANT_NBR\": - \"NON-GRANT\", \"LINE_ITEM\": \"1\", \"MULTI_CURR_FLAG\": \"N\", \"OUTSTANDING_DCT\": - \"0\", \"OUTSTANDING_DCT_DC\": \"0\", \"OVERALL_AMOUNT\": \"50000\", \"OVERALL_AMOUNT_DC\": - \"2707500\", \"VENDOR_CODE\": \"PVN\", \"WBS_ELEMENT\": \"9999\\\/A0\\\/05\\\/803\\\/005\\\/004\" - }, {\"ACTUAL_CASH_TRANSFER\": \"50197.02\", \"ACTUAL_CASH_TRANSFER_DC\": \"2689656.62\", - \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": \"PHP\", \"CURRENT_FR_AMOUNT\": \"50197.02\", - \"DONOR_CODE\": \"CODE X\", \"DONOR_NAME\": \"DONOR Z\", \"DUE_DATE\": \"17-OCT-18\", - \"FR_DOC_DATE\": \"17-OCT-18\", \"FR_DOCUMENT_TEXT\": \"FR FOR MRM PD NP\", - \"FR_END_DATE\": \"31-MAR-19\", \"FR_LINE_ITEM_TEXT\": \"FR FOR NP\", \"FR_NUMBER\": - \"9999\", \"FR_OVERALL_AMOUNT\": \"179643.93\", \"FR_START_DATE\": \"17-OCT-18\", - \"FR_TYPE\": \"Programme Document Against PCA\", \"FUND\": \"SC\", \"GRANT_NBR\": - \"GRANT W\", \"LINE_ITEM\": \"3\", \"MULTI_CURR_FLAG\": \"N\", \"OUTSTANDING_DCT\": - \"0\", \"OUTSTANDING_DCT_DC\": \"0\", \"OVERALL_AMOUNT\": \"25125.48\", \"OVERALL_AMOUNT_DC\": - \"1360545.62\", \"VENDOR_CODE\": \"PVN\", \"WBS_ELEMENT\": \"9999\\\/A0\\\/05\\\/803\\\/005\\\/004\" - } ] }}"' + string: '"{ \"ROWSET\": { \"ROW\": [ {\"ACTUAL_CASH_TRANSFER\": \"383131\", + \"ACTUAL_CASH_TRANSFER_DC\": \"383131\", \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": + \"USD\", \"CURRENT_FR_AMOUNT\": \"383131\", \"DONOR_CODE\": \"G45301\", \"DONOR_NAME\": + \"Macioce\", \"DUE_DATE\": \"01-FEB-17\", \"FR_DOC_DATE\": \"17-JAN-17\", + \"FR_DOCUMENT_TEXT\": \"WASH NZ:EM LIF SAVIMG IN KHAZER \\\/ACF 1ST TRANCHE\", + \"FR_END_DATE\": \"31-MAR-17\", \"FR_LINE_ITEM_TEXT\": \"WASH NZ:EM LIF SAVIMG + IN KHAZER \\\/ACF 1ST TRANCHE\", \"FR_NUMBER\": \"9999\", \"FR_OVERALL_AMOUNT\": + \"383131\", \"FR_START_DATE\": \"17-JAN-17\", \"FR_TYPE\": \"Programme Document + Against PCA\", \"FUND\": \"SM\", \"GRANT_NBR\": \"XYZ\", \"LINE_ITEM\": + \"2\", \"MULTI_CURR_FLAG\": \"N\", \"OUTSTANDING_DCT\": \"0\", \"OUTSTANDING_DCT_DC\": + \"0\", \"OVERALL_AMOUNT\": \"246176\", \"OVERALL_AMOUNT_DC\": \"246176\", + \"VENDOR_CODE\": \"PVN\", \"WBS_ELEMENT\": \"2130\\\/A0\\\/08\\\/002\\\/005\\\/312\" + }, {\"ACTUAL_CASH_TRANSFER\": \"383131\", \"ACTUAL_CASH_TRANSFER_DC\": \"383131\", + \"COMPLETED_FLAG\": \"N\", \"CURRENCY\": \"USD\", \"CURRENT_FR_AMOUNT\": \"383131\", + \"DONOR_CODE\": \"G45605\", \"DONOR_NAME\": \"USA (USAID) OFDA\", \"DUE_DATE\": + \"01-FEB-17\", \"FR_DOC_DATE\": \"17-JAN-17\", \"FR_DOCUMENT_TEXT\": \"XYA + NZ:EM LIF ZAOJ \\\/ACF 1ST TRANCHE\", \"FR_END_DATE\": \"31-MAR-17\", + \"FR_LINE_ITEM_TEXT\": \"WASH NZ:EM LIF SAVIMG IN KHAZER \\\/ACF 1ST TRANCHE\", + \"FR_NUMBER\": \"9999\", \"FR_OVERALL_AMOUNT\": \"383131\", \"FR_START_DATE\": + \"17-JAN-17\", \"FR_TYPE\": \"Programme Document Against PCA\", \"FUND\": + \"SM\", \"GRANT_NBR\": \"SM160255\", \"LINE_ITEM\": \"1\", \"MULTI_CURR_FLAG\": + \"N\", \"OUTSTANDING_DCT\": \"0\", \"OUTSTANDING_DCT_DC\": \"0\", \"OVERALL_AMOUNT\": + \"136955\", \"OVERALL_AMOUNT_DC\": \"136955\", \"VENDOR_CODE\": \"PVN\", + \"WBS_ELEMENT\": \"2130\\\/A0\\\/08\\\/002\\\/005\\\/312\" } ] }}"' headers: Content-Type: - application/json; charset=utf-8 Date: - - Wed, 19 Jun 2019 20:40:54 GMT + - Mon, 22 Jul 2019 19:02:08 GMT Server: - Microsoft-IIS/8.5 Microsoft-HTTPAPI/2.0 Vary: @@ -81,7 +52,7 @@ interactions: X-Powered-By: - ASP.NET content-length: - - '4433' + - '1934' status: code: 200 message: OK diff --git a/src/etools/applications/t2f/serializers/travel.py b/src/etools/applications/t2f/serializers/travel.py index 9795d893a2..8b820f989e 100644 --- a/src/etools/applications/t2f/serializers/travel.py +++ b/src/etools/applications/t2f/serializers/travel.py @@ -360,7 +360,7 @@ def update_object(self, obj, data): related_manager.add(*value) def update_related_objects(self, attr_name, related_data): - many = isinstance(self._fields[attr_name], serializers.ListSerializer) + many = isinstance(self.fields[attr_name], serializers.ListSerializer) try: related = getattr(self.instance, attr_name) @@ -371,7 +371,7 @@ def update_related_objects(self, attr_name, related_data): # Load the queryset related = related.all() - model = self._fields[attr_name].child.Meta.model + model = self.fields[attr_name].child.Meta.model related_to_delete = {o.pk for o in related} # Iterate over incoming data and create/update the models From f4dd3f6d1ba765fd6f026e8eb1e703112b0afaf7 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 22 Jul 2019 17:21:27 -0400 Subject: [PATCH 06/56] shell_plus imports --- src/etools/config/settings/base.py | 4 ++++ src/etools/config/settings/local.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index 7dcb0e69c8..43818cd5cf 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -564,3 +564,7 @@ def before_send(event, hint): GEOS_LIBRARY_PATH = os.getenv('GEOS_LIBRARY_PATH', '/usr/lib/libgeos_c.so.1') # default path GDAL_LIBRARY_PATH = os.getenv('GDAL_LIBRARY_PATH', '/usr/lib/libgdal.so.20') # default path + +SHELL_PLUS_PRE_IMPORTS = ( + ('etools.applications.core.util_scripts', '*'), +) diff --git a/src/etools/config/settings/local.py b/src/etools/config/settings/local.py index 5c5e384e5a..0611630acf 100644 --- a/src/etools/config/settings/local.py +++ b/src/etools/config/settings/local.py @@ -90,7 +90,3 @@ } LOGGING['handlers']['console']['filters'] = ['tenant_context'] LOGGING['handlers']['console']['formatter'] = 'tenant_context' - -SHELL_PLUS_PRE_IMPORTS = ( - ('etools.applications.core.util_scripts', '*'), -) From fa4850579258ede537e88899dd7449ab1eeaacfa Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 22 Jul 2019 17:42:48 -0400 Subject: [PATCH 07/56] fix tests --- Pipfile | 2 +- Pipfile.lock | 8 ++++---- src/etools/applications/tpm/views.py | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Pipfile b/Pipfile index eaf8b68334..d6c1638350 100644 --- a/Pipfile +++ b/Pipfile @@ -39,7 +39,7 @@ 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-model-utils = "==3.2" django-ordered-model = "==3.3" django-post_office = "==3.2.1" django-redis-cache = "==2.0" diff --git a/Pipfile.lock b/Pipfile.lock index c107315693..da018a7bf3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "23ab3bf6e0fd9bee31c6086127cd44227aea5c83d85cee73f283bb48537f223a" + "sha256": "d7abae18990d2f0e3d138cc3f2b0fa08a4df2e8304723ae9f8af6e8006a6dff8" }, "pipfile-spec": 6, "requires": { @@ -893,11 +893,11 @@ }, "django-model-utils": { "hashes": [ - "sha256:2c057f3bf0859aba27f04389f0cedd2d48f8c9b3848acb86fd9970794e58f477", - "sha256:8cd377744aa45f9f131d652ec460c57d1aaa88d3e9b586c8e27eb709341b9084" + "sha256:3f130a262e45d73e0950d2be76af4bf4ee86804dd60e5f90afc5cd948fcfe760", + "sha256:682f58c1de330cedcda58cc85d5232c5b47a9e2cb67bef4541fb43fdaeb18e96" ], "index": "pypi", - "version": "==3.1.2" + "version": "==3.2.0" }, "django-mptt": { "hashes": [ diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index 5817f8f3c1..2ee169bcdc 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -310,8 +310,7 @@ class TPMVisitViewSet( viewsets.GenericViewSet ): metadata_class = PermissionBasedMetadata - queryset = TPMVisit.objects.all().prefetch_related( - 'tpm_partner', + queryset = TPMVisit.objects.select_related('tpm_partner').prefetch_related( 'tpm_activities__unicef_focal_points', ) serializer_class = TPMVisitSerializer From 9ce9d61dd6c3eea02979541fdce95f7179c083bc Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 26 Jul 2019 15:04:29 -0400 Subject: [PATCH 08/56] fix tests --- tox.ini | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index c4ee6ce4a8..0daeb0ee1f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = d{21,22} +envlist = d{22} [testenv] basepython=python3.7 @@ -17,12 +17,6 @@ whitelist_externals = pipenv commands = pipenv install --dev --ignore-pipfile -[testenv:d21] -commands = - {[testenv]commands} - pip install "django>=2.1,<2.2" - ./runtests.sh - [testenv:d22] commands = {[testenv]commands} From 68615d4ae95368839fbd959c864bd289ca2642ab Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 31 Jul 2019 16:41:06 -0400 Subject: [PATCH 09/56] 10175 tpm approve permission --- .../tpm/management/commands/update_tpm_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 14fdc3875d..24e351f95d 100644 --- a/src/etools/applications/tpm/management/commands/update_tpm_permissions.py +++ b/src/etools/applications/tpm/management/commands/update_tpm_permissions.py @@ -299,7 +299,7 @@ def assign_permissions(self): condition=tpm_reported_condition) self.add_permissions(self.pme, 'edit', ['tpm.tpmvisit.approval_comment', 'tpm.tpmvisit.report_reject_comments'], condition=tpm_reported_condition) - self.add_permissions(self.pme, 'action', + self.add_permissions([self.pme, self.focal_point], 'action', ['tpm.tpmvisit.approve', 'tpm.tpmvisit.reject_report'], condition=tpm_reported_condition) From 423c40bf85598563efcf571b8816406d2b10f37e Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 1 Aug 2019 15:22:11 -0400 Subject: [PATCH 10/56] 13377 tpm activity is_pv export --- src/etools/applications/tpm/export/renderers.py | 6 ++++-- src/etools/applications/tpm/export/serializers.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/tpm/export/renderers.py b/src/etools/applications/tpm/export/renderers.py index e2c036b845..ead721aa4d 100644 --- a/src/etools/applications/tpm/export/renderers.py +++ b/src/etools/applications/tpm/export/renderers.py @@ -3,17 +3,19 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework_csv.renderers import CSVRenderer +from unicef_rest_export.renderers import FriendlyCSVRenderer -class TPMActivityCSVRenderer(CSVRenderer): +class TPMActivityCSVRenderer(FriendlyCSVRenderer): header = ['ref', 'visit', 'visit_status', 'activity', 'section', 'cp_output', 'partner', 'intervention', 'pd_ssfa', - 'locations', 'date', 'unicef_focal_points', 'offices', 'tpm_focal_points', 'visit_information', + 'locations', 'date', 'unicef_focal_points', 'offices', 'tpm_focal_points', 'visit_information', 'is_pv', 'additional_information', 'link'] labels = { 'ref': _('Visit Ref. #'), 'visit': _('Visit'), 'visit_status': _('Status of Visit'), 'activity': _('Task'), + 'is_pv': _('Is Programmatic Visit'), 'section': _('Section'), 'cp_output': _('PD/SSFA output'), 'partner': _('Partner'), diff --git a/src/etools/applications/tpm/export/serializers.py b/src/etools/applications/tpm/export/serializers.py index 2a92d2677f..dcc196c8a2 100644 --- a/src/etools/applications/tpm/export/serializers.py +++ b/src/etools/applications/tpm/export/serializers.py @@ -24,6 +24,7 @@ class TPMActivityExportSerializer(serializers.Serializer): visit_information = serializers.CharField(source='tpm_visit.visit_information') visit_status = serializers.CharField(source='tpm_visit.get_status_display') activity = serializers.SerializerMethodField() + is_pv = serializers.BooleanField() section = serializers.CharField() cp_output = serializers.CharField() partner = serializers.CharField() From 2c4bdd23493e6866813c7331d57896b8204b2046 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 1 Aug 2019 15:24:50 -0400 Subject: [PATCH 11/56] version 7.3 --- src/etools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/__init__.py b/src/etools/__init__.py index 2a5f0703c8..2d564128c4 100644 --- a/src/etools/__init__.py +++ b/src/etools/__init__.py @@ -1,2 +1,2 @@ -VERSION = __version__ = '7.2' +VERSION = __version__ = '7.3' NAME = 'eTools' From 54fc21c58970d86fcc69b0ebe2b146fd59a0f6d8 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 2 Aug 2019 14:36:33 -0400 Subject: [PATCH 12/56] fix tests --- src/etools/applications/tpm/tests/test_transitions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/tpm/tests/test_transitions.py b/src/etools/applications/tpm/tests/test_transitions.py index fe2d54f6dc..011121f754 100644 --- a/src/etools/applications/tpm/tests/test_transitions.py +++ b/src/etools/applications/tpm/tests/test_transitions.py @@ -249,7 +249,10 @@ def setUpTestData(cls): class FPPermissionsForTpmTransitionTestCase(TPMTransitionPermissionsTestCase): - ALLOWED_TRANSITION = [] + ALLOWED_TRANSITION = [ + ('tpm_reported', 'reject_report'), + ('tpm_reported', 'approve'), + ] @classmethod def setUpTestData(cls): From e3496777139cdc5f5fa71a51284d0272cfd89906 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 2 Aug 2019 16:13:45 -0400 Subject: [PATCH 13/56] 12877 hact dashboard spot checks: change MR to required --- src/etools/applications/hact/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/hact/models.py b/src/etools/applications/hact/models.py index 4e0b9f62d8..be147f04cc 100644 --- a/src/etools/applications/hact/models.py +++ b/src/etools/applications/hact/models.py @@ -231,7 +231,7 @@ def get_assurance_activities(self): Q(partner__reported_cy__gt=0) | Q(partner__total_ct_cy__gt=0), partner__hidden=False, date_of_draft_report_to_ip__year=datetime.now().year).exclude( status=Engagement.CANCELLED).count(), - 'min_required': sum([p.min_req_spot_checks for p in self.get_queryset()]), + 'required': sum([p.planned_engagement.spot_check_required for p in self.get_queryset()]), 'follow_up': self.get_queryset().aggregate(total=Coalesce(Sum( 'planned_engagement__spot_check_follow_up'), 0))['total'] }, From 5b7453dcab1fcc0bf9842c1d71e9f1bc60aea2c7 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 2 Aug 2019 16:28:10 -0400 Subject: [PATCH 14/56] 9465 engagement allow to filter by partner_contacted_at date --- src/etools/applications/audit/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/applications/audit/filters.py b/src/etools/applications/audit/filters.py index 3f0824af2c..1ebb3012e1 100644 --- a/src/etools/applications/audit/filters.py +++ b/src/etools/applications/audit/filters.py @@ -73,4 +73,5 @@ class Meta: 'joint_audit': ['exact'], 'agreement__auditor_firm__unicef_users_allowed': ['exact'], 'staff_members__user': ['exact', 'in'], + 'partner_contacted_at': ['lte', 'gte', 'gt', 'lt'], } From 06fe34697849c383befdfd35db4264750d0536da Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 2 Aug 2019 16:47:18 -0400 Subject: [PATCH 15/56] fix tests --- src/etools/applications/hact/models.py | 3 ++- src/etools/applications/hact/tests/test_models.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/etools/applications/hact/models.py b/src/etools/applications/hact/models.py index be147f04cc..8886bc2ea4 100644 --- a/src/etools/applications/hact/models.py +++ b/src/etools/applications/hact/models.py @@ -231,7 +231,8 @@ def get_assurance_activities(self): Q(partner__reported_cy__gt=0) | Q(partner__total_ct_cy__gt=0), partner__hidden=False, date_of_draft_report_to_ip__year=datetime.now().year).exclude( status=Engagement.CANCELLED).count(), - 'required': sum([p.planned_engagement.spot_check_required for p in self.get_queryset()]), + 'required': sum([p.planned_engagement.spot_check_required for p in self.get_queryset().filter( + planned_engagement__isnull=False)]), 'follow_up': self.get_queryset().aggregate(total=Coalesce(Sum( 'planned_engagement__spot_check_follow_up'), 0))['total'] }, diff --git a/src/etools/applications/hact/tests/test_models.py b/src/etools/applications/hact/tests/test_models.py index e0e833afc7..a974bcbf0c 100644 --- a/src/etools/applications/hact/tests/test_models.py +++ b/src/etools/applications/hact/tests/test_models.py @@ -1,4 +1,3 @@ - from datetime import datetime from etools.applications.audit.models import Audit, Engagement @@ -12,7 +11,7 @@ from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.hact.tests.factories import AggregateHactFactory from etools.applications.partners.models import PartnerOrganization, PartnerType -from etools.applications.partners.tests.factories import PartnerFactory +from etools.applications.partners.tests.factories import PartnerFactory, PlannedEngagementFactory class TestAggregateHact(BaseTenantTestCase): @@ -45,6 +44,8 @@ def setUpTestData(cls): reported_cy=52000.0, total_ct_ytd=550000.0, ) + PlannedEngagementFactory(partner=cls.partner, spot_check_follow_up=3) + PlannedEngagementFactory(partner=cls.partner2, spot_check_follow_up=2) AuditFactory( status=Engagement.FINAL, @@ -126,7 +127,7 @@ def test_get_assurance_activities(self): self.assertEqual(assurance_activities['programmatic_visits']['completed'], 0) self.assertEqual(assurance_activities['programmatic_visits']['min_required'], 5) self.assertEqual(assurance_activities['spot_checks']['completed'], 0) - self.assertEqual(assurance_activities['spot_checks']['min_required'], 6) + self.assertEqual(assurance_activities['spot_checks']['required'], 6) self.assertEqual(assurance_activities['scheduled_audit'], 1) self.assertEqual(assurance_activities['special_audit'], 1) self.assertEqual(assurance_activities['micro_assessment'], 1) From c1c3755d58c82eaf3a30db90a7cba452c2e2d7b6 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 23 Jul 2019 16:03:19 -0400 Subject: [PATCH 16/56] pin docutils --- Pipfile | 5 +- Pipfile.lock | 262 +++++++++++++++++++++++++++------------------------ 2 files changed, 144 insertions(+), 123 deletions(-) diff --git a/Pipfile b/Pipfile index d6c1638350..9081b70e48 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ carto = "==1.6" celery = "==4.3" dj-database-url = "==0.5" dj-static = "==0.0.6" -Django = "==2.2.3" +Django = "==2.2.4" django-appconf = "==1.0.3" django_celery_beat = "==1.5" django-celery-email = "==2.0.2" @@ -38,7 +38,7 @@ 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-logentry-admin = "==1.0.5" django-model-utils = "==3.2" django-ordered-model = "==3.3" django-post_office = "==3.2.1" @@ -77,6 +77,7 @@ unicef_snapshot = "==0.2.3" unicef-rest-export = "==0.5.3" xhtml2pdf = "==0.2.3" unicef-vision = "*" +docutils = "==0.15" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index da018a7bf3..62e8fa3266 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d7abae18990d2f0e3d138cc3f2b0fa08a4df2e8304723ae9f8af6e8006a6dff8" + "sha256": "abc904a56bfdc80e34ef551f3d63675da385fe10dbc9048e52b4d518cac276b7" }, "pipfile-spec": 6, "requires": { @@ -762,11 +762,11 @@ }, "django": { "hashes": [ - "sha256:4d23f61b26892bac785f07401bc38cbf8fa4cec993f400e9cd9ddf28fd51c0ea", - "sha256:6e974d4b57e3b29e4882b244d40171d6a75202ab8d2402b8e8adbd182e25cf0c" + "sha256:16a5d54411599780ac9dfe3b9b38f90f785c51259a584e0b24b6f14a7f69aae8", + "sha256:9a2f98211ab474c710fcdad29c82f30fc14ce9917c7a70c3682162a624de4035" ], "index": "pypi", - "version": "==2.2.3" + "version": "==2.2.4" }, "django-appconf": { "hashes": [ @@ -886,10 +886,10 @@ }, "django-logentry-admin": { "hashes": [ - "sha256:0033fa146b5c3d1195a1d306e5b665a22b901450e55715f3dec50c2b4076f8f6" + "sha256:e47e94f7778be85eae91f1f1bc8ac56475f38d297f800df3f7afacd780a9ff24" ], "index": "pypi", - "version": "==1.0.4" + "version": "==1.0.5" }, "django-model-utils": { "hashes": [ @@ -1017,6 +1017,14 @@ "index": "pypi", "version": "==1.4" }, + "docutils": { + "hashes": [ + "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", + "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" + ], + "index": "pypi", + "version": "==0.15" + }, "drf-nested-routers": { "hashes": [ "sha256:46e5c3abc15c782cafafd7d75028e8f9121bbc6228e3599bbb48a3daa4585034", @@ -1130,32 +1138,30 @@ }, "lxml": { "hashes": [ - "sha256:06c7616601430aa140a69f97e3116308fffe0848f543b639a5ec2e8920ae72fd", - "sha256:177202792f9842374a8077735c69c41a4282183f7851443d2beb8ee310720819", - "sha256:19317ad721ceb9e39847d11131903931e2794e447d4751ebb0d9236f1b349ff2", - "sha256:36d206e62f3e5dbaafd4ec692b67157e271f5da7fd925fda8515da675eace50d", - "sha256:387115b066c797c85f9861a9613abf50046a15aac16759bc92d04f94acfad082", - "sha256:3ce1c49d4b4a7bc75fb12acb3a6247bb7a91fe420542e6d671ba9187d12a12c2", - "sha256:4d2a5a7d6b0dbb8c37dab66a8ce09a8761409c044017721c21718659fa3365a1", - "sha256:58d0a1b33364d1253a88d18df6c0b2676a1746d27c969dc9e32d143a3701dda5", - "sha256:62a651c618b846b88fdcae0533ec23f185bb322d6c1845733f3123e8980c1d1b", - "sha256:69ff21064e7debc9b1b1e2eee8c2d686d042d4257186d70b338206a80c5bc5ea", - "sha256:7060453eba9ba59d821625c6af6a266bd68277dce6577f754d1eb9116c094266", - "sha256:7d26b36a9c4bce53b9cfe42e67849ae3c5c23558bc08363e53ffd6d94f4ff4d2", - "sha256:83b427ad2bfa0b9705e02a83d8d607d2c2f01889eb138168e462a3a052c42368", - "sha256:923d03c84534078386cf50193057aae98fa94cace8ea7580b74754493fda73ad", - "sha256:b773715609649a1a180025213f67ffdeb5a4878c784293ada300ee95a1f3257b", - "sha256:baff149c174e9108d4a2fee192c496711be85534eab63adb122f93e70aa35431", - "sha256:bca9d118b1014b4c2d19319b10a3ebed508ff649396ce1855e1c96528d9b2fa9", - "sha256:ce580c28845581535dc6000fc7c35fdadf8bea7ccb57d6321b044508e9ba0685", - "sha256:d34923a569e70224d88e6682490e24c842907ba2c948c5fd26185413cbe0cd96", - "sha256:dd9f0e531a049d8b35ec5e6c68a37f1ba6ec3a591415e6804cbdf652793d15d7", - "sha256:ecb805cbfe9102f3fd3d2ef16dfe5ae9e2d7a7dfbba92f4ff1e16ac9784dbfb0", - "sha256:ede9aad2197a0202caff35d417b671f5f91a3631477441076082a17c94edd846", - "sha256:ef2d1fc370400e0aa755aab0b20cf4f1d0e934e7fd5244f3dd4869078e4942b9", - "sha256:f2fec194a49bfaef42a548ee657362af5c7a640da757f6f452a35da7dd9f923c" - ], - "version": "==4.3.4" + "sha256:06e5599b9c54f797a3c0f384c67705a0d621031007aa2400a6c7d17300fdb995", + "sha256:092237cfe4ece074401b75001a2e525fa6e1fb9d40fee8b7b132b1947d3bd2f8", + "sha256:0b6d49d0a26fe8207df8dd27c40b75be4deb2277173903aa76ec3e82df77cbe7", + "sha256:0f77061c20b4f32b1cf39e8f661c74e966344084c996e7b23c3a94e472461df0", + "sha256:0fef86edfa2f146b4b0ae2c6c05c3e4a8f3388b3655eafbc4aab3247f4dabb24", + "sha256:2f163c8844db4ed06a230ef092e2461ad01830972a896b8f3cf8b5bac70ae85d", + "sha256:350333190052bbfbc3222b1805b59b7979d7276e57af2257367e15a2db27082d", + "sha256:3b57dc5ed7b6a7d852c961f2389ca99404c2b59fd2088baec6fbaca02f688be4", + "sha256:3e86e5df4a8edd6f725f3c76f1d45e046d4f3aa40478092e4f5f373ad1f526e2", + "sha256:43dac60d10341d3e56be089cd0798b70e70d45ce32279f4c3190d8cbd71350e4", + "sha256:4665ee84ac8ba11d58f1ed517e29ea8536b4ae4e0c6fb6c7d3dce70abcd279f0", + "sha256:5033cf606a7cb559db967689b1b2e743994000f783607ba4c484e90917395ad7", + "sha256:75d731af05bf40f808d7716e0d26b4b02913402f861c032ce8c36efca350ae72", + "sha256:7720174604c7647e357566ac9e4d135c137caed5e7b01223551a4c81c8dc8b9a", + "sha256:b33ec641309bcea40c76c1b105f988e4e8f9a2f1ee1486aa5c0eeef33956c9bb", + "sha256:d1135dc0ac197242028ede085b693ba1f2bff7f0f9b91080e2540348312bfa53", + "sha256:d5a61e9c2322b45f259909a02b76bc98c4641214e22a37191d00c151aa9cdb9a", + "sha256:da22c4b17bc17dad9c8faf6d94c8fe568ac71c867a56631ab874da418fc7f8f7", + "sha256:da5c48ec9f8d8b5df42d328b6d1fb8d9413cd664a2367ef4f6f7cc48ee5b82c0", + "sha256:db2794bad21b7b30b6849b4e1537171cae8a7087711d958d69c233470dc612e7", + "sha256:f1c2f67df727034f94ccb590142d1d110f3dd38f638a4f1567fdd9f39892ba05", + "sha256:f840dddded8b046edc774c88ed8d2442cdb231a68894c42c74e3a809450fae76" + ], + "version": "==4.4.0" }, "markupsafe": { "hashes": [ @@ -1192,10 +1198,10 @@ }, "msrest": { "hashes": [ - "sha256:2c0909570913785a4408a17286e151f3b28d39277113e5c63378572f7395c660", - "sha256:c9e9cbb0c47745f9f5c82cce60849d7c3ec9e33fc6fad9e2987b7657ad1ba479" + "sha256:27589fb400da7e1a98778688f70a0099e4fc6fea59d0f4835b4fbdad3bb8a6d9", + "sha256:cda706a2ccfb032cf41fa8cc6575cbca29634fed2d226fc789e4a8daf44ab7c1" ], - "version": "==0.6.8" + "version": "==0.6.9" }, "msrestazure": { "hashes": [ @@ -1384,33 +1390,35 @@ }, "pytz": { "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", + "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" ], - "version": "==2019.1" + "version": "==2019.2" }, "pyyaml": { "hashes": [ - "sha256:57acc1d8533cbe51f6662a55434f0dbecfa2b9eaf115bede8f6fd00115a0c0d3", - "sha256:588c94b3d16b76cfed8e0be54932e5729cc185caffaa5a451e7ad2f7ed8b4043", - "sha256:68c8dd247f29f9a0d09375c9c6b8fdc64b60810ebf07ba4cdd64ceee3a58c7b7", - "sha256:70d9818f1c9cd5c48bb87804f2efc8692f1023dac7f1a1a5c61d454043c1d265", - "sha256:86a93cccd50f8c125286e637328ff4eef108400dd7089b46a7be3445eecfa391", - "sha256:a0f329125a926876f647c9fa0ef32801587a12328b4a3c741270464e3e4fa778", - "sha256:a3c252ab0fa1bb0d5a3f6449a4826732f3eb6c0270925548cac342bc9b22c225", - "sha256:b4bb4d3f5e232425e25dda21c070ce05168a786ac9eda43768ab7f3ac2770955", - "sha256:cd0618c5ba5bda5f4039b9398bb7fb6a317bb8298218c3de25c47c4740e4b95e", - "sha256:ceacb9e5f8474dcf45b940578591c7f3d960e82f926c707788a570b51ba59190", - "sha256:fe6a88094b64132c4bb3b631412e90032e8cfe9745a58370462240b8cb7553cd" - ], - "version": "==5.1.1" + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" }, "redis": { "hashes": [ - "sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175", - "sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273" + "sha256:8106502b96280e8614d30766f28b4e07ca2b368a8d7896bffce9de88756dd481", + "sha256:95ccbec607e21fff2823e7da8b7041e0c471eb36d0672f20abcb32adb5b089ca" ], - "version": "==3.2.1" + "version": "==3.3.5" }, "reportlab": { "hashes": [ @@ -1675,6 +1683,13 @@ ], "version": "==0.7.12" }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, "babel": { "hashes": [ "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", @@ -1705,40 +1720,41 @@ }, "coverage": { "hashes": [ - "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", - "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", - "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", - "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", - "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", - "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", - "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", - "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", - "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", - "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", - "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", - "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", - "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", - "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", - "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", - "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", - "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", - "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", - "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", - "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", - "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", - "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", - "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", - "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", - "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", - "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", - "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", - "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", - "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", - "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", - "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" - ], - "index": "pypi", - "version": "==4.5.3" + "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", + "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", + "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", + "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", + "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", + "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", + "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", + "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", + "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", + "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", + "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", + "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", + "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", + "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", + "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", + "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", + "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", + "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", + "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", + "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", + "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", + "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", + "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", + "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", + "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", + "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", + "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", + "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", + "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", + "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", + "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", + "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" + ], + "index": "pypi", + "version": "==4.5.4" }, "decorator": { "hashes": [ @@ -1757,9 +1773,11 @@ }, "docutils": { "hashes": [ - "sha256:ba4584f9107571ced0d2c7f56a5499c696215ba90797849c92d395979da68521" + "sha256:54a349c622ff31c91cbec43b0b512f113b5b24daf00e2ea530bb1bd9aac14849", + "sha256:d2ddba74835cb090a1b627d3de4e7835c628d07ee461f7b4480f51af2fe4d448" ], - "version": "==0.15.post1" + "index": "pypi", + "version": "==0.15" }, "drf-api-checker": { "hashes": [ @@ -1835,18 +1853,18 @@ }, "importlib-metadata": { "hashes": [ - "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", - "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" + "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", + "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" ], - "version": "==0.18" + "version": "==0.19" }, "ipython": { "hashes": [ - "sha256:11067ab11d98b1e6c7f0993506f7a5f8a91af420f7e82be6575fcb7a6ca372a0", - "sha256:60bc55c2c1d287161191cc2469e73c116d9b634cff25fe214a43cba7cec94c79" + "sha256:1d3a1692921e932751bc1a1f7bb96dc38671eeefdc66ed33ee4cbc57e92a410e", + "sha256:537cd0176ff6abd06ef3e23f2d0c4c2c8a4d9277b7451544c6cbf56d1c79a83d" ], "index": "pypi", - "version": "==7.6.1" + "version": "==7.7.0" }, "ipython-genutils": { "hashes": [ @@ -1961,10 +1979,10 @@ }, "packaging": { "hashes": [ - "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", - "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", + "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" ], - "version": "==19.0" + "version": "==19.1" }, "parso": { "hashes": [ @@ -2047,10 +2065,10 @@ }, "pyparsing": { "hashes": [ - "sha256:530d8bf8cc93a34019d08142593cf4d78a05c890da8cf87ffa3120af53772238", - "sha256:f78e99616b6f1a4745c0580e170251ef1bbafc0d0513e270c4bd281bf29d2800" + "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", + "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" ], - "version": "==2.4.1" + "version": "==2.4.2" }, "python-dateutil": { "hashes": [ @@ -2061,26 +2079,28 @@ }, "pytz": { "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", + "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" ], - "version": "==2019.1" + "version": "==2019.2" }, "pyyaml": { "hashes": [ - "sha256:57acc1d8533cbe51f6662a55434f0dbecfa2b9eaf115bede8f6fd00115a0c0d3", - "sha256:588c94b3d16b76cfed8e0be54932e5729cc185caffaa5a451e7ad2f7ed8b4043", - "sha256:68c8dd247f29f9a0d09375c9c6b8fdc64b60810ebf07ba4cdd64ceee3a58c7b7", - "sha256:70d9818f1c9cd5c48bb87804f2efc8692f1023dac7f1a1a5c61d454043c1d265", - "sha256:86a93cccd50f8c125286e637328ff4eef108400dd7089b46a7be3445eecfa391", - "sha256:a0f329125a926876f647c9fa0ef32801587a12328b4a3c741270464e3e4fa778", - "sha256:a3c252ab0fa1bb0d5a3f6449a4826732f3eb6c0270925548cac342bc9b22c225", - "sha256:b4bb4d3f5e232425e25dda21c070ce05168a786ac9eda43768ab7f3ac2770955", - "sha256:cd0618c5ba5bda5f4039b9398bb7fb6a317bb8298218c3de25c47c4740e4b95e", - "sha256:ceacb9e5f8474dcf45b940578591c7f3d960e82f926c707788a570b51ba59190", - "sha256:fe6a88094b64132c4bb3b631412e90032e8cfe9745a58370462240b8cb7553cd" - ], - "version": "==5.1.1" + "sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", + "sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", + "sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", + "sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", + "sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", + "sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", + "sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", + "sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", + "sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", + "sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", + "sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", + "sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", + "sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8" + ], + "version": "==5.1.2" }, "requests": { "hashes": [ @@ -2207,10 +2227,10 @@ }, "virtualenv": { "hashes": [ - "sha256:861bbce3a418110346c70f5c7a696fdcf23a261424e1d28aa4f9362fc2ccbc19", - "sha256:ba8ce6a961d842320681fb90a3d564d0e5134f41dacd0e2bae7f02441dde2d52" + "sha256:6cb2e4c18d22dbbe283d0a0c31bb7d90771a606b2cb3415323eea008eaee6a9d", + "sha256:909fe0d3f7c9151b2df0a2cb53e55bdb7b0d61469353ff7a49fd47b0f0ab9285" ], - "version": "==16.6.2" + "version": "==16.7.2" }, "wcwidth": { "hashes": [ From 17164c81328630f849683c50a9abce09bea9c8e6 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 2 Aug 2019 15:55:23 -0400 Subject: [PATCH 17/56] fix tests --- .../applications/action_points/views.py | 1 + .../applications/management/handlers.py | 212 +++++++++++++ .../migrations/0003_sectionhistory.py | 33 ++ .../migrations/0004_auto_20190715_2047.py | 22 ++ .../management/migrations_issues.py | 26 ++ src/etools/applications/management/models.py | 26 ++ .../management/tests/test_handlers.py | 177 +++++++++++ src/etools/applications/management/urls.py | 8 + .../applications/management/views/sections.py | 65 ++++ .../partners/serializers/interventions_v2.py | 22 ++ .../TestAPIIntervention/fixtures.json | 208 ++++++------- .../get__api_v2_interventions_.response.json | 26 +- ...t__api_v2_interventions_101_.response.json | 284 +++++++++--------- ...v2_interventions_amendments_.response.json | 20 +- ...ventions_applied-indicators_.response.json | 55 ++++ ...v2_interventions_indicators_.response.json | 4 +- ...t__api_v2_interventions_map_.response.json | 36 +-- .../applications/partners/tests/test_api.py | 2 + src/etools/applications/partners/urls_v2.py | 5 + .../partners/views/interventions_v2.py | 17 ++ .../reports/migrations/0018_section_active.py | 18 ++ src/etools/applications/reports/models.py | 1 + .../applications/reports/serializers/v2.py | 8 + .../applications/t2f/filters/__init__.py | 7 +- src/etools/config/settings/base.py | 9 - 25 files changed, 994 insertions(+), 298 deletions(-) create mode 100644 src/etools/applications/management/handlers.py create mode 100644 src/etools/applications/management/migrations/0003_sectionhistory.py create mode 100644 src/etools/applications/management/migrations/0004_auto_20190715_2047.py create mode 100644 src/etools/applications/management/migrations_issues.py create mode 100644 src/etools/applications/management/models.py create mode 100644 src/etools/applications/management/tests/test_handlers.py create mode 100644 src/etools/applications/management/views/sections.py create mode 100644 src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_applied-indicators_.response.json create mode 100644 src/etools/applications/reports/migrations/0018_section_active.py diff --git a/src/etools/applications/action_points/views.py b/src/etools/applications/action_points/views.py index 9190a8c059..0689547a70 100644 --- a/src/etools/applications/action_points/views.py +++ b/src/etools/applications/action_points/views.py @@ -88,6 +88,7 @@ class ActionPointViewSet( )} filter_fields.update({ 'status': ['exact', 'in'], + 'section': ['exact', 'in'], 'due_date': ['exact', 'lte', 'gte'] }) diff --git a/src/etools/applications/management/handlers.py b/src/etools/applications/management/handlers.py new file mode 100644 index 0000000000..052ee23182 --- /dev/null +++ b/src/etools/applications/management/handlers.py @@ -0,0 +1,212 @@ +from django.db.transaction import atomic + +from etools.applications.action_points.models import ActionPoint +from etools.applications.management.models import SectionHistory +from etools.applications.partners.models import Intervention +from etools.applications.reports.models import AppliedIndicator, Section +from etools.applications.t2f.models import Travel +from etools.applications.tpm.models import TPMActivity, TPMVisit + + +class MigrationException(Exception): + """Exception thrown when migration is failing due validation""" + + +class NotDeactivatedException(MigrationException): + """Exception thrown when deactivated is still referenced""" + + +class IndicatorSectionInconsistentException(MigrationException): + """Exception thrown when indicator's section is not in the intervention""" + + +class SectionHandler: + + FK = 'fk' + M2M = 'm2m' + + intervention_updatable_status = [ + Intervention.DRAFT, + Intervention.SIGNED, + Intervention.ACTIVE, + Intervention.ENDED, + Intervention.SUSPENDED + ] + travel_updatable_status = [ + Travel.PLANNED, + Travel.SUBMITTED, + Travel.REJECTED, + Travel.APPROVED + ] + tpm_visit_updatable_status = [ + TPMVisit.DRAFT, + TPMVisit.ASSIGNED, + TPMVisit.ACCEPTED, + TPMVisit.REJECTED, + TPMVisit.REPORTED, + TPMVisit.REPORT_REJECTED + ] + + # dictionary with info related to the model, active queryset (object relevant to the migration) + # attribute name and type of relation + queryset_migration_mapping = { + 'interventions': ( + Intervention.objects.filter(status__in=intervention_updatable_status), + 'sections', + M2M + ), + 'applied_indicators': ( + AppliedIndicator.objects.filter(lower_result__result_link__intervention__status__in=intervention_updatable_status), + 'section', + FK + ), + 'travels': ( + Travel.objects.filter(status__in=travel_updatable_status), + 'section', + FK + ), + 'tpm_activities': ( + TPMActivity.objects.filter(tpm_visit__status__in=tpm_visit_updatable_status), + 'section', + FK + ), + 'action_points': ( + ActionPoint.objects.filter(status=ActionPoint.STATUS_OPEN), + 'section', + FK + ) + } + + @staticmethod + @atomic + def new(new_section_name): + """Create new section""" + section = Section.objects.create(name=new_section_name) + + # history + section_history = SectionHistory.objects.create(history_type=SectionHistory.CREATE) + section_history.to_sections.set([section]) + return section + + @staticmethod + @atomic + def merge(new_section_name, sections_to_merge): + """Create two or more sections into a new section migrating active objects""" + from_instances = Section.objects.filter(pk__in=sections_to_merge) + to_instance = Section.objects.create(name=new_section_name) + from_instances.update(active=False) + + # m2m relation need to be cleaned at the end + m2m_to_clean = { + 'interventions': [] + } + for from_instance in from_instances: + SectionHandler.__update_objects(from_instance, to_instance, m2m_to_clean) + SectionHandler.__clean_m2m(from_instances, m2m_to_clean) + + # history + section_history = SectionHistory.objects.create(history_type=SectionHistory.MERGE) + section_history.from_sections.set(from_instances) + section_history.to_sections.set([to_instance, ]) + + return to_instance + + @staticmethod + @atomic + def close(from_instance_pk, new_section_2_new_querysets): + """ + Close a section and split the active objects according the mapping dictionary + new_section_2_new_querysets has a mapping of the instances to be mapped to new section + """ + + from_instance = Section.objects.get(pk=from_instance_pk) + from_instance.active = False + from_instance.save() + + new_sections = [] + # m2m relation need to be cleaned at the end + m2m_to_clean = { + 'interventions': [] + } + + for new_section_name, queryset_mapping_dict in new_section_2_new_querysets.items(): + to_instance, _ = Section.objects.get_or_create(name=new_section_name) + new_sections.append(to_instance) + SectionHandler.__update_objects(from_instance, to_instance, m2m_to_clean, queryset_mapping_dict) + + SectionHandler.__clean_m2m([from_instance], m2m_to_clean) + + # history + section_history = SectionHistory.objects.create(history_type=SectionHistory.CLOSE) + section_history.from_sections.set([from_instance]) + section_history.to_sections.set(new_sections) + + SectionHandler.__disabled_section_check(from_instance) + SectionHandler.__consistent_indicators_check(new_sections) + + return new_sections + + @staticmethod + def __update_objects(from_instance, to_instance, m2m_to_clean, section_split_dict=None): + """ + update eTools queryset from one instance to another + section_split_dict has the redistribution mapping after a section is closed + """ + for model_key in SectionHandler.queryset_migration_mapping.keys(): + qs, section_attribute, relation = SectionHandler.queryset_migration_mapping[model_key] + + if section_split_dict: # if it's a close we filter the queryset + instance_pks = section_split_dict[model_key] if section_split_dict else [] + qs = qs.filter(pk__in=instance_pks) + + if relation == SectionHandler.M2M: + to_update = SectionHandler.__update_m2m(qs, section_attribute, from_instance, to_instance) + m2m_to_clean[model_key].extend(to_update) + + elif relation == SectionHandler.FK: + SectionHandler.__update_fk(qs, section_attribute, from_instance, to_instance) + + @staticmethod + def __update_fk(qs, section_attribute, old_section, new_section): + """Updates qs when section is a ForeignKey""" + qs.filter(**{section_attribute: old_section}).update(**{section_attribute: new_section}) + + @staticmethod + def __update_m2m(qs, section_attribute, old_section, new_section): + """Updates qs when section is a ManyToManyField""" + qs = qs.filter(**{section_attribute: old_section}) + for instance in qs: + attr = getattr(instance, section_attribute) + attr.add(new_section) + return qs.values_list('pk', flat=True) + + @staticmethod + def __clean_m2m(from_instances, m2m_to_clean): + """ + Clean m2m fields from old sections + NOTE: This step has to be done when the add of all section has been completed + """ + for model_key, to_clean in m2m_to_clean.items(): + qs, section_attribute, relation = SectionHandler.queryset_migration_mapping[model_key] + for instance in qs.filter(pk__in=to_clean): + attr = getattr(instance, section_attribute) + for from_instance in from_instances: + attr.remove(from_instance) + + @staticmethod + def __disabled_section_check(old_section): + """Checks that the old section is not referenced by any active objects""" + for _, (qs, attribute_name, _) in SectionHandler.queryset_migration_mapping.items(): + if qs.filter(**{attribute_name: old_section}).exists(): + raise NotDeactivatedException(f'{qs.model.__name__} in active status exists') + + @staticmethod + def __consistent_indicators_check(new_sections): + """Checks that the old section is not referenced by any active objects""" + interventions = Intervention.objects.filter(sections__in=new_sections) + for intervention in interventions: + applied_indicator_sections = set(AppliedIndicator.objects.filter( + lower_result__result_link__intervention=intervention).values_list('section', flat=True).distinct()) + intervention_sections = set(intervention.sections.values_list('pk', flat=True)) + if not applied_indicator_sections.issubset(intervention_sections): + raise IndicatorSectionInconsistentException(f'Intervention {intervention.pk} has inconsistent indicators') diff --git a/src/etools/applications/management/migrations/0003_sectionhistory.py b/src/etools/applications/management/migrations/0003_sectionhistory.py new file mode 100644 index 0000000000..ef6e508596 --- /dev/null +++ b/src/etools/applications/management/migrations/0003_sectionhistory.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.3 on 2019-07-09 19:39 + +from django.db import migrations, models +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('reports', '0018_section_active'), + ('management', '0002_auto_20190326_1500'), + ] + + operations = [ + migrations.CreateModel( + name='SectionHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('history_type', models.CharField(max_length=500, verbose_name='Name')), + ('permission_type', models.CharField(choices=[('create', 'Create'), ('merge', 'Merge'), ('split', 'Split'), ('close', 'Close')], max_length=10)), + ('from_sections', models.ManyToManyField(blank=True, related_name='history_from', to='reports.Section', verbose_name='From')), + ('to_sections', models.ManyToManyField(blank=True, related_name='history_to', to='reports.Section', verbose_name='To')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/etools/applications/management/migrations/0004_auto_20190715_2047.py b/src/etools/applications/management/migrations/0004_auto_20190715_2047.py new file mode 100644 index 0000000000..e64688d940 --- /dev/null +++ b/src/etools/applications/management/migrations/0004_auto_20190715_2047.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.3 on 2019-07-15 20:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0003_sectionhistory'), + ] + + operations = [ + migrations.RemoveField( + model_name='sectionhistory', + name='permission_type', + ), + migrations.AlterField( + model_name='sectionhistory', + name='history_type', + field=models.CharField(choices=[('create', 'Create'), ('merge', 'Merge'), ('split', 'Split'), ('close', 'Close')], max_length=10, verbose_name='Name'), + ), + ] diff --git a/src/etools/applications/management/migrations_issues.py b/src/etools/applications/management/migrations_issues.py new file mode 100644 index 0000000000..50b5edb7b8 --- /dev/null +++ b/src/etools/applications/management/migrations_issues.py @@ -0,0 +1,26 @@ +from etools.applications.core.util_scripts import set_country +from etools.applications.reports.models import Indicator, Result +from etools.applications.users.models import Country + +model = Result +for workspace in Country.objects.exclude(schema_name='public'): + set_country(workspace.name) + if model.objects.filter(sector__isnull=False).exists(): + print(model.objects.filter(sector__isnull=False).count(), workspace) + +# 5 China +# 21 FRG +# 2 Sierra Leone +# 55 UAT + +model = Indicator +for workspace in Country.objects.exclude(schema_name='public'): + set_country(workspace.name) + if model.objects.filter(sector__isnull=False).exists(): + print(model.objects.filter(sector__isnull=False).count(), workspace) + +# 2 Cambodia +# 1 China +# 8 FRG +# 16 Sierra Leone +# 27 UAT diff --git a/src/etools/applications/management/models.py b/src/etools/applications/management/models.py new file mode 100644 index 0000000000..ccb6c9602e --- /dev/null +++ b/src/etools/applications/management/models.py @@ -0,0 +1,26 @@ +from django.db import models +from django.utils.translation import ugettext as _ + +from model_utils.models import TimeStampedModel + +from etools.applications.reports.models import Section + + +class SectionHistory(TimeStampedModel): + """ Model to keep history of Section changes""" + CREATE = 'create' + MERGE = 'merge' + SPLIT = 'split' + CLOSE = 'close' + + TYPES = ( + (CREATE, 'Create'), + (MERGE, 'Merge'), + (SPLIT, 'Split'), + (CLOSE, 'Close'), + ) + + from_sections = models.ManyToManyField(Section, blank=True, verbose_name=_('From'), related_name='history_from') + to_sections = models.ManyToManyField(Section, blank=True, verbose_name=_('To'), related_name='history_to') + + history_type = models.CharField(verbose_name=_("Name"), max_length=10, choices=TYPES) diff --git a/src/etools/applications/management/tests/test_handlers.py b/src/etools/applications/management/tests/test_handlers.py new file mode 100644 index 0000000000..632d1ec11a --- /dev/null +++ b/src/etools/applications/management/tests/test_handlers.py @@ -0,0 +1,177 @@ +from etools.applications.action_points.models import ActionPoint +from etools.applications.action_points.tests.factories import ActionPointFactory +from etools.applications.core.tests.cases import BaseTenantTestCase +from etools.applications.management.handlers import ( + IndicatorSectionInconsistentException, + NotDeactivatedException, + SectionHandler, +) +from etools.applications.management.models import SectionHistory +from etools.applications.partners.models import Intervention +from etools.applications.partners.tests.factories import InterventionFactory, InterventionResultLinkFactory +from etools.applications.reports.models import AppliedIndicator, Section +from etools.applications.reports.tests.factories import AppliedIndicatorFactory, SectionFactory +from etools.applications.t2f.models import Travel +from etools.applications.t2f.tests.factories import TravelFactory + + +class TestSectionHandler(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.health = SectionFactory(name='Health') + cls.nutrition = SectionFactory(name='Nutrition') + cls.ict = SectionFactory(name='ICT') + + cls.intervention1 = InterventionFactory(status=Intervention.SIGNED) + cls.intervention2 = InterventionFactory(status=Intervention.CLOSED) + cls.intervention3 = InterventionFactory(status=Intervention.SIGNED) + + cls.intervention1.sections.set([cls.health, cls.nutrition]) + cls.intervention2.sections.set([cls.nutrition, cls.health]) + cls.intervention3.sections.set([cls.health, cls.nutrition, cls.ict]) + + cls.applied_indicator1 = AppliedIndicatorFactory( + lower_result__result_link=InterventionResultLinkFactory(intervention=cls.intervention1), + section=cls.health) + cls.applied_indicator2 = AppliedIndicatorFactory( + lower_result__result_link=InterventionResultLinkFactory(intervention=cls.intervention1), + section=cls.nutrition) + cls.applied_indicator3 = AppliedIndicatorFactory( + lower_result__result_link=InterventionResultLinkFactory(intervention=cls.intervention2), + section=cls.health) + cls.applied_indicator4 = AppliedIndicatorFactory( + lower_result__result_link=InterventionResultLinkFactory(intervention=cls.intervention3), + section=cls.ict) + cls.applied_indicator5 = AppliedIndicatorFactory( + lower_result__result_link=InterventionResultLinkFactory(intervention=cls.intervention3), + section=cls.health) + + cls.travel1 = TravelFactory(status=Travel.SUBMITTED, section=cls.health) + cls.travel2 = TravelFactory(status=Travel.REJECTED, section=cls.health) + cls.travel3 = TravelFactory(status=Travel.SUBMITTED, section=cls.ict) + TravelFactory(status=Travel.COMPLETED, section=cls.nutrition) + TravelFactory(status=Travel.COMPLETED, section=cls.health) + + cls.action_point = ActionPointFactory(status=ActionPoint.STATUS_OPEN, section=cls.health) + ActionPointFactory(status=ActionPoint.STATUS_COMPLETED, section=cls.nutrition) + + def __check_section(self, section, counts): + """Utility method to make more thin and readable tests""" + interventions, applied_indicators, travels, action_points = counts + self.assertEqual(Intervention.objects.filter(sections=section).count(), interventions) + self.assertEqual(AppliedIndicator.objects.filter(section=section).count(), applied_indicators) + self.assertEqual(Travel.objects.filter(section=section).count(), travels) + self.assertEqual(ActionPoint.objects.filter(section=section).count(), action_points) + + def __check_before(self): + """Checks the status before running the test""" + self.__check_section(self.health, (3, 3, 3, 1)) + self.__check_section(self.nutrition, (3, 1, 1, 1)) + self.__check_section(self.ict, (1, 1, 1, 0)) + + def __check_history(self, sections, deactivated, history_count, history_type, from_count, to_count): + """Checks the status before running the test""" + self.assertEqual(Section.objects.count(), sections) + self.assertEqual(Section.objects.filter(active=False).count(), deactivated) + self.assertEqual(SectionHistory.objects.count(), history_count) + if history_count: + self.assertEqual(SectionHistory.objects.first().history_type, history_type) + self.assertEqual(SectionHistory.objects.first().from_sections.count(), from_count) + self.assertEqual(SectionHistory.objects.first().to_sections.count(), to_count) + + def test_new(self): + self.__check_history(3, 0, 0, None, None, None) + SectionHandler.new('New Section') + self.__check_history(4, 0, 1, SectionHistory.CREATE, 0, 1) + self.assertEqual(SectionHistory.objects.first().to_sections.first().name, 'New Section') + + def test_merge(self): + self.__check_history(3, 0, 0, None, None, None) + self.__check_before() + + SectionHandler.merge('Health and Nutrition', [self.health.pk, self.nutrition.pk]) + + h_n = Section.objects.get(name='Health and Nutrition') + self.__check_history(4, 2, 1, SectionHistory.MERGE, 2, 1) + self.__check_section(self.health, (1, 1, 1, 0)) + self.__check_section(self.nutrition, (1, 0, 1, 1)) + self.__check_section(h_n, (2, 3, 2, 1)) + self.__check_section(self.ict, (1, 1, 1, 0)) + + def test_close_ok(self): + self.__check_history(3, 0, 0, None, None, None) + self.__check_before() + close_dict = { + "Health 1": { + 'interventions': [self.intervention1.pk, self.intervention3.pk], + 'applied_indicators': [self.applied_indicator5.pk], + 'travels': [self.travel2.pk, ], + 'tpm_activities': [], + 'action_points': [self.action_point.pk], + }, + "Health 2": { + 'interventions': [self.intervention1.pk], + 'applied_indicators': [self.applied_indicator1.pk], + 'travels': [self.travel1.pk, self.travel2.pk], + 'tpm_activities': [], + 'action_points': [], + } + } + + SectionHandler.close(self.health.pk, close_dict) + + hea = Section.objects.get(name='Health 1') + lth = Section.objects.get(name='Health 2') + + self.__check_history(5, 1, 1, SectionHistory.CLOSE, 1, 2) + self.__check_section(self.health, (1, 1, 1, 0)) + self.__check_section(self.nutrition, (3, 1, 1, 1)) + self.__check_section(hea, (2, 1, 1, 1)) + self.__check_section(lth, (1, 1, 1, 0)) # travel2 is updated twice last one stays + self.__check_section(self.ict, (1, 1, 1, 0)) + + def test_close_fail_not_migrated(self): + self.__check_history(3, 0, 0, None, None, None) + self.__check_before() + close_dict = { + "Health 1": { + 'interventions': [self.intervention1.pk], + 'applied_indicators': [self.applied_indicator5.pk], + 'travels': [self.travel2.pk, ], + 'tpm_activities': [], + 'action_points': [self.action_point.pk], + }, + "Health 2": { + 'interventions': [self.intervention1.pk], + 'applied_indicators': [self.applied_indicator1.pk], + 'travels': [self.travel1.pk, self.travel2.pk], + 'tpm_activities': [], + 'action_points': [], + } + } + with self.assertRaises(NotDeactivatedException): + SectionHandler.close(self.health.pk, close_dict) + self.__check_before() + + def test_close_fail_inconsistent_section(self): + self.__check_history(3, 0, 0, None, None, None) + self.__check_before() + close_dict = { + "Hea": { + 'interventions': [self.intervention1.pk, self.intervention3.pk], + 'applied_indicators': [self.applied_indicator1.pk], + 'travels': [self.travel2.pk, ], + 'tpm_activities': [], + 'action_points': [self.action_point.pk], + }, + "Lth": { + 'interventions': [self.intervention1.pk], + 'applied_indicators': [self.applied_indicator5.pk], + 'travels': [self.travel1.pk, self.travel2.pk], + 'tpm_activities': [], + 'action_points': [], + } + } + with self.assertRaises(IndicatorSectionInconsistentException): + SectionHandler.close(self.health.pk, close_dict) + self.__check_before() diff --git a/src/etools/applications/management/urls.py b/src/etools/applications/management/urls.py index 95b35251a5..661594eeed 100644 --- a/src/etools/applications/management/urls.py +++ b/src/etools/applications/management/urls.py @@ -1,7 +1,11 @@ from django.conf.urls import url +from django.urls import include + +from rest_framework import routers from etools.applications.management.views.general import InvalidateCache, SyncCountries, SyncFRs from etools.applications.management.views.reports import LoadResultStructure +from etools.applications.management.views.sections import SectionsManagementView from etools.applications.management.views.tasks_endpoints import ( PMPIndicatorsReportView, SyncAllUsers, @@ -13,6 +17,9 @@ ) from etools.applications.management.views.v1 import ActiveUsers, AgreementsStatisticsView, PortalDashView +router = routers.DefaultRouter() +router.register(r'sections', SectionsManagementView, base_name='sections_management') + app_name = 'management' @@ -32,4 +39,5 @@ url(r'^tasks/send_test_email/$', TestSendEmailAPIView.as_view(), name='tasks_send_test_email'), url(r'^reports/users/$', UsersReportView.as_view(), name='reports_users'), url(r'^reports/pmp_indicators/$', PMPIndicatorsReportView.as_view(), name='reports_pmp_indicators'), + url(r'^', include(router.urls)), ), 'management') diff --git a/src/etools/applications/management/views/sections.py b/src/etools/applications/management/views/sections.py new file mode 100644 index 0000000000..d4d3dc4506 --- /dev/null +++ b/src/etools/applications/management/views/sections.py @@ -0,0 +1,65 @@ +import logging + +from django.db import IntegrityError +from django.utils.translation import ugettext as _ + +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from etools.applications.management.handlers import SectionHandler + +logger = logging.getLogger(__name__) + + +class SectionsManagementView(viewsets.ViewSet): + """Class for handling session creation, merging and closing""" + + @action(detail=False, methods=['post']) + def new(self, request): + try: + new_section_name = request.data['new_section_name'] + except KeyError: + return Response(_('Unable to unpack'), status=status.HTTP_400_BAD_REQUEST) + + try: + section = SectionHandler.new(new_section_name) + except IntegrityError: + return Response(f'Section {new_section_name} already exist', status=status.HTTP_400_BAD_REQUEST) + + return Response({ + 'pk': section.pk, + 'name': section.name, + }) + + @action(detail=False, methods=['post']) + def merge(self, request): + try: + new_section_name = request.data['new_section_name'] + sections_to_merge = request.data['sections_to_merge'] + logger.info('Section to Merge', sections_to_merge) + except KeyError: + return Response(_('Unable to unpack'), status=status.HTTP_400_BAD_REQUEST) + try: + section = SectionHandler.merge(new_section_name, sections_to_merge) + except IntegrityError: + return Response(f'Section {new_section_name} already exist', status=status.HTTP_400_BAD_REQUEST) + + return Response({ + 'pk': section.pk, + 'name': section.name, + }) + + @action(detail=False, methods=['post']) + def close(self, request): + try: + old_section = request.data['old_section'] + objects_dict = request.data['new_sections'] + + logger.info('Section Migrate', objects_dict) + except KeyError: + return Response(_('Unable to unpack'), status=status.HTTP_400_BAD_REQUEST) + + sections = SectionHandler.close(old_section, objects_dict) + data = [{'pk': section.pk, 'name': section.name} for section in sections] + return Response(data) diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 211ee7a5e8..09d9fcee0f 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -28,6 +28,7 @@ from etools.applications.partners.permissions import InterventionPermissions from etools.applications.reports.models import AppliedIndicator, LowerResult, ReportingRequirement from etools.applications.reports.serializers.v2 import ( + AppliedIndicatorBasicSerializer, IndicatorSerializer, LowerResultCUSerializer, LowerResultSerializer, @@ -265,6 +266,27 @@ class Meta: ) +class BasicInterventionListSerializer(serializers.ModelSerializer): + partner_name = serializers.CharField(source='agreement.partner.name') + indicators = serializers.SerializerMethodField() + + def get_indicators(self, obj): + qs = [ai for rl in obj.result_links.all() for ll in rl.ll_results.all() for ai in ll.applied_indicators.all()] + return AppliedIndicatorBasicSerializer(qs, many=True).data + + class Meta: + model = Intervention + fields = ( + 'id', + 'title', + 'number', + 'partner_name', + 'status', + 'indicators', + 'sections' + ) + + class InterventionAttachmentSerializer(AttachmentSerializerMixin, serializers.ModelSerializer): attachment_file = serializers.FileField(source="attachment", read_only=True) attachment_document = AttachmentSingleFileField( diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/fixtures.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/fixtures.json index 2129ba09e8..e58ec5c14d 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/fixtures.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/fixtures.json @@ -4,19 +4,19 @@ "model": "partners.intervention", "pk": 101, "fields": { - "created": "2018-12-05T16:22:57.668Z", - "modified": "2018-12-05T16:22:57.681Z", + "created": "2019-07-01T20:32:43.418Z", + "modified": "2019-07-01T20:32:43.421Z", "document_type": "", - "agreement": 599, + "agreement": 1572, "country_programme": null, - "number": "TST/PCA2018599/2018101", - "title": "Intervention Title 1", + "number": "TST/PCA20191572/2019101", + "title": "Intervention Title 0", "status": "draft", "start": null, "end": null, - "submission_date": "2018-12-05", + "submission_date": "2019-07-01", "submission_date_prc": null, - "reference_number_year": 2018, + "reference_number_year": 2019, "review_date_prc": null, "prc_review_document": "", "signed_pd_document": "", @@ -40,22 +40,22 @@ "deps": [ { "model": "partners.agreement", - "pk": 599, + "pk": 1572, "fields": { - "created": "2018-12-05T16:22:57.653Z", - "modified": "2018-12-05T16:22:57.666Z", - "partner": 572, - "country_programme": 546, + "created": "2019-07-01T20:32:43.395Z", + "modified": "2019-07-01T20:32:43.417Z", + "partner": 1496, + "country_programme": 1448, "agreement_type": "PCA", - "agreement_number": "TST/PCA2018599", + "agreement_number": "TST/PCA20191572", "attached_agreement": "", - "start": "2018-12-05", - "end": "2018-12-31", - "reference_number_year": 2018, + "start": "2019-07-01", + "end": "2019-12-31", + "reference_number_year": 2019, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-12-05", + "signed_by_unicef_date": "2019-07-01", "signed_by": null, - "signed_by_partner_date": "2018-12-05", + "signed_by_partner_date": "2019-07-01", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -63,13 +63,13 @@ }, { "model": "partners.partnerorganization", - "pk": 572, + "pk": 1496, "fields": { - "created": "2018-12-05T16:22:57.648Z", - "modified": "2018-12-05T16:22:57.651Z", + "created": "2019-07-01T20:32:43.381Z", + "modified": "2019-07-01T20:32:43.387Z", "partner_type": "", "cso_type": null, - "name": "Partner 5", + "name": "Partner 0", "short_name": "", "description": "", "shared_with": null, @@ -138,13 +138,13 @@ }, { "model": "reports.countryprogramme", - "pk": 546, + "pk": 1448, "fields": { - "name": "Country Programme 5", - "wbs": "0000/A0/05", + "name": "Country Programme 0", + "wbs": "0000/A0/00", "invalid": false, - "from_date": "2018-01-01", - "to_date": "2018-12-31" + "from_date": "2019-01-01", + "to_date": "2019-12-31" } } ] @@ -152,36 +152,36 @@ "amendment": { "master": { "model": "partners.interventionamendment", - "pk": 35, + "pk": 42, "fields": { - "created": "2018-12-05T16:22:57.709Z", - "modified": "2018-12-05T16:22:57.726Z", - "intervention": 508, - "types": "[\"Change IP name\"]", - "other_description": "VJppZjLpmvqgiEiisnLUDfGzhjjeCDBpaJYLdRdZTNYwevzYtd", - "signed_date": "2018-12-05", + "created": "2019-07-01T20:32:43.465Z", + "modified": "2019-07-01T20:32:43.501Z", + "intervention": 1455, + "types": "[\"Change authorized officer\"]", + "other_description": "pxRKBufTiSdqiGAOyXwghKOpNAMJCXkyyvpGCFZerSqsemMYaY", + "signed_date": "2019-07-01", "amendment_number": 1, - "signed_amendment": "test/file_attachments/partner_organization/573/573/agreements/600/interventions/508/amendments/None/test_file.pdf" + "signed_amendment": "test/file_attachments/partner_organization/1497/1497/agreements/1573/interventions/1455/amendments/None/test_file.pdf" } }, "deps": [ { "model": "partners.intervention", - "pk": 508, + "pk": 1455, "fields": { - "created": "2018-12-05T16:22:57.707Z", - "modified": "2018-12-05T16:22:57.725Z", + "created": "2019-07-01T20:32:43.453Z", + "modified": "2019-07-01T20:32:43.497Z", "document_type": "", - "agreement": 600, + "agreement": 1573, "country_programme": null, - "number": "TST/PCA2018600/2018508", - "title": "Intervention Title 2", + "number": "TST/PCA20191573/20191455", + "title": "Intervention Title 1", "status": "draft", "start": null, "end": null, - "submission_date": "2018-12-05", + "submission_date": "2019-07-01", "submission_date_prc": null, - "reference_number_year": 2018, + "reference_number_year": 2019, "review_date_prc": null, "prc_review_document": "", "signed_pd_document": "", @@ -204,22 +204,22 @@ }, { "model": "partners.agreement", - "pk": 600, + "pk": 1573, "fields": { - "created": "2018-12-05T16:22:57.692Z", - "modified": "2018-12-05T16:22:57.706Z", - "partner": 573, - "country_programme": 547, + "created": "2019-07-01T20:32:43.437Z", + "modified": "2019-07-01T20:32:43.451Z", + "partner": 1497, + "country_programme": 1449, "agreement_type": "PCA", - "agreement_number": "TST/PCA2018600", + "agreement_number": "TST/PCA20191573", "attached_agreement": "", - "start": "2018-12-05", - "end": "2018-12-31", - "reference_number_year": 2018, + "start": "2019-07-01", + "end": "2019-12-31", + "reference_number_year": 2019, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-12-05", + "signed_by_unicef_date": "2019-07-01", "signed_by": null, - "signed_by_partner_date": "2018-12-05", + "signed_by_partner_date": "2019-07-01", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -227,13 +227,13 @@ }, { "model": "partners.partnerorganization", - "pk": 573, + "pk": 1497, "fields": { - "created": "2018-12-05T16:22:57.683Z", - "modified": "2018-12-05T16:22:57.687Z", + "created": "2019-07-01T20:32:43.426Z", + "modified": "2019-07-01T20:32:43.432Z", "partner_type": "", "cso_type": null, - "name": "Partner 6", + "name": "Partner 1", "short_name": "", "description": "", "shared_with": null, @@ -302,13 +302,13 @@ }, { "model": "reports.countryprogramme", - "pk": 547, + "pk": 1449, "fields": { - "name": "Country Programme 6", - "wbs": "0000/A0/06", + "name": "Country Programme 1", + "wbs": "0000/A0/01", "invalid": false, - "from_date": "2018-01-01", - "to_date": "2018-12-31" + "from_date": "2019-01-01", + "to_date": "2019-12-31" } } ] @@ -316,33 +316,33 @@ "result": { "master": { "model": "partners.interventionresultlink", - "pk": 93, + "pk": 889, "fields": { - "created": "2018-12-05T16:22:57.747Z", - "modified": "2018-12-05T16:22:57.748Z", - "intervention": 509, - "cp_output": 137, + "created": "2019-07-01T20:32:43.555Z", + "modified": "2019-07-01T20:32:43.555Z", + "intervention": 1456, + "cp_output": 953, "ram_indicators": [] } }, "deps": [ { "model": "partners.intervention", - "pk": 509, + "pk": 1456, "fields": { - "created": "2018-12-05T16:22:57.741Z", - "modified": "2018-12-05T16:22:57.742Z", + "created": "2019-07-01T20:32:43.538Z", + "modified": "2019-07-01T20:32:43.541Z", "document_type": "", - "agreement": 601, + "agreement": 1574, "country_programme": null, - "number": "TST/PCA2018601/2018509", - "title": "Intervention Title 3", + "number": "TST/PCA20191574/20191456", + "title": "Intervention Title 2", "status": "draft", "start": null, "end": null, - "submission_date": "2018-12-05", + "submission_date": "2019-07-01", "submission_date_prc": null, - "reference_number_year": 2018, + "reference_number_year": 2019, "review_date_prc": null, "prc_review_document": "", "signed_pd_document": "", @@ -365,22 +365,22 @@ }, { "model": "partners.agreement", - "pk": 601, + "pk": 1574, "fields": { - "created": "2018-12-05T16:22:57.732Z", - "modified": "2018-12-05T16:22:57.740Z", - "partner": 574, - "country_programme": 548, + "created": "2019-07-01T20:32:43.510Z", + "modified": "2019-07-01T20:32:43.537Z", + "partner": 1498, + "country_programme": 1450, "agreement_type": "PCA", - "agreement_number": "TST/PCA2018601", + "agreement_number": "TST/PCA20191574", "attached_agreement": "", - "start": "2018-12-05", - "end": "2018-12-31", - "reference_number_year": 2018, + "start": "2019-07-01", + "end": "2019-12-31", + "reference_number_year": 2019, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-12-05", + "signed_by_unicef_date": "2019-07-01", "signed_by": null, - "signed_by_partner_date": "2018-12-05", + "signed_by_partner_date": "2019-07-01", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -388,13 +388,13 @@ }, { "model": "partners.partnerorganization", - "pk": 574, + "pk": 1498, "fields": { - "created": "2018-12-05T16:22:57.728Z", - "modified": "2018-12-05T16:22:57.730Z", + "created": "2019-07-01T20:32:43.504Z", + "modified": "2019-07-01T20:32:43.507Z", "partner_type": "", "cso_type": null, - "name": "Partner 7", + "name": "Partner 2", "short_name": "", "description": "", "shared_with": null, @@ -463,28 +463,30 @@ }, { "model": "reports.countryprogramme", - "pk": 548, + "pk": 1450, "fields": { - "name": "Country Programme 7", - "wbs": "0000/A0/07", + "name": "Country Programme 2", + "wbs": "0000/A0/02", "invalid": false, - "from_date": "2018-01-01", - "to_date": "2018-12-31" + "from_date": "2019-01-01", + "to_date": "2019-12-31" } }, { "model": "reports.result", - "pk": 137, + "pk": 953, "fields": { "country_programme": null, - "result_type": 118, + "result_type": 915, "sector": null, "name": "Result 0", "code": null, - "from_date": "2018-01-01", - "to_date": "2018-12-31", + "from_date": "2019-01-01", + "to_date": "2019-12-31", "parent": null, "humanitarian_tag": false, + "humanitarian_marker_code": null, + "humanitarian_marker_name": null, "wbs": null, "vision_id": null, "gic_code": null, @@ -495,8 +497,8 @@ "activity_focus_name": null, "hidden": false, "ram": false, - "created": "2018-12-05T16:22:57.745Z", - "modified": "2018-12-05T16:22:57.746Z", + "created": "2019-07-01T20:32:43.549Z", + "modified": "2019-07-01T20:32:43.552Z", "lft": 1, "rght": 2, "tree_id": 1, @@ -505,7 +507,7 @@ }, { "model": "reports.resulttype", - "pk": 118, + "pk": 915, "fields": { "name": "ResultType 0" } diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_.response.json index f4c5c938fe..fddeeabe97 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_.response.json @@ -19,17 +19,17 @@ ], "content-length": [ "Content-Length", - "2404" + "2411" ] }, "data": [ { - "id": 509, - "number": "TST/PCA2018601/2018509", + "id": 1456, + "number": "TST/PCA20191574/20191456", "document_type": "", - "partner_name": "Partner 7", + "partner_name": "Partner 2", "status": "draft", - "title": "Intervention Title 3", + "title": "Intervention Title 2", "start": null, "end": null, "frs_total_frs_amt": null, @@ -41,7 +41,7 @@ "sections": [], "section_names": [], "cp_outputs": [ - 137 + 953 ], "unicef_focal_points": [], "frs_total_intervention_amt": null, @@ -64,12 +64,12 @@ "grants": [] }, { - "id": 508, - "number": "TST/PCA2018600/2018508", + "id": 1455, + "number": "TST/PCA20191573/20191455", "document_type": "", - "partner_name": "Partner 6", + "partner_name": "Partner 1", "status": "draft", - "title": "Intervention Title 2", + "title": "Intervention Title 1", "start": null, "end": null, "frs_total_frs_amt": null, @@ -103,11 +103,11 @@ }, { "id": 101, - "number": "TST/PCA2018599/2018101", + "number": "TST/PCA20191572/2019101", "document_type": "", - "partner_name": "Partner 5", + "partner_name": "Partner 0", "status": "draft", - "title": "Intervention Title 1", + "title": "Intervention Title 0", "start": null, "end": null, "frs_total_frs_amt": null, diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_101_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_101_.response.json index 2c2fdba8f2..cc4f91c2f7 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_101_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_101_.response.json @@ -19,16 +19,16 @@ ], "content-length": [ "Content-Length", - "5533" + "5536" ] }, "data": { "id": 101, "frs": [], - "partner": "Partner 5", - "agreement": 599, + "partner": "Partner 0", + "agreement": 1572, "document_type": "", - "number": "TST/PCA2018599/2018101", + "number": "TST/PCA20191572/2019101", "prc_review_document_file": null, "frs_details": { "frs": [], @@ -42,13 +42,13 @@ "multi_curr_flag": false }, "signed_pd_document_file": null, - "title": "Intervention Title 1", + "title": "Intervention Title 0", "status": "draft", "start": null, "end": null, "submission_date_prc": null, "review_date_prc": null, - "submission_date": "2018-12-05", + "submission_date": "2019-07-01", "prc_review_document": null, "submitted_to_prc": false, "signed_pd_document": null, @@ -60,8 +60,8 @@ "offices": [], "population_focus": null, "signed_by_partner_date": null, - "created": "2018-12-05T16:22:57.668000Z", - "modified": "2018-12-05T16:22:57.681000Z", + "created": "2019-07-01T20:32:43.418000Z", + "modified": "2019-07-01T20:32:43.421000Z", "planned_budget": null, "result_links": [], "country_programme": null, @@ -71,175 +71,175 @@ "attachments": [], "permissions": { "edit": { - "reporting_requirements": false, - "signed_pd_attachment": false, - "activation_letter_attachment": false, - "partner_authorized_officer_signatory_id": true, - "metadata": true, - "reference_number_year": true, - "prc_review_attachment": false, - "attachments": true, - "modified": true, - "status": true, - "result_links": false, - "planned_budget": false, - "review_date_prc": false, - "in_amendment": false, - "end": false, - "amendments": false, - "prc_review_document": false, - "unicef_signatory_id": true, - "agreement": false, - "actionpoint": true, "start": false, - "sections": false, - "partner_authorized_officer_signatory": false, - "signed_by_partner_date": false, - "title": false, + "document_type": false, + "activation_letter": false, + "offices": false, + "engagement": true, + "agreement": false, + "submission_date": false, + "signed_by_unicef_date": false, + "in_amendment": false, + "planned_budget": false, + "result_links": false, + "number": false, + "prc_review_attachment": false, + "activation_letter_attachment": false, "submission_date_prc": false, "id": true, + "unicef_focal_points": false, "special_reporting_requirements": true, - "country_programme": false, - "contingency_pd": false, - "termination_doc_attachment": true, - "unicef_signatory": false, + "reporting_periods": true, + "title": false, + "termination_doc": false, "frs": false, + "partner_authorized_officer_signatory": false, + "sections": false, + "signed_pd_document": false, + "unicef_signatory": false, + "termination_doc_attachment": true, + "signed_by_partner_date": false, + "planned_visits": false, + "actionpoint": true, "created": true, + "contingency_pd": false, + "partner_authorized_officer_signatory_id": true, + "prc_review_document": false, + "end": false, + "activity": true, + "country_programme": false, "country_programme_id": true, - "submission_date": false, - "planned_visits": false, - "termination_doc": false, - "flat_locations": true, - "unicef_focal_points": false, - "activation_letter": false, - "signed_by_unicef_date": false, - "reporting_periods": true, + "amendments": false, + "reference_number_year": true, + "partner_focal_points": false, + "unicef_signatory_id": true, "travel_activities": true, - "population_focus": true, + "metadata": true, + "modified": true, + "attachments": true, "agreement_id": true, - "activity": true, - "partner_focal_points": false, - "offices": false, - "signed_pd_document": false, - "document_type": false, - "number": false, - "engagement": true, + "flat_locations": true, + "reporting_requirements": false, + "review_date_prc": false, + "signed_pd_attachment": false, + "status": true, + "population_focus": true, "sections_present": true }, "required": { - "reporting_requirements": false, - "signed_pd_attachment": false, - "activation_letter_attachment": false, - "partner_authorized_officer_signatory_id": false, - "metadata": false, - "reference_number_year": true, - "prc_review_attachment": false, - "attachments": false, - "modified": false, - "status": false, - "result_links": false, - "planned_budget": false, - "review_date_prc": false, - "in_amendment": false, - "end": false, - "amendments": false, - "prc_review_document": false, - "unicef_signatory_id": false, - "agreement": true, - "actionpoint": false, "start": false, - "sections": false, - "partner_authorized_officer_signatory": false, - "signed_by_partner_date": false, - "title": true, + "document_type": true, + "activation_letter": false, + "offices": false, + "engagement": false, + "agreement": true, + "submission_date": false, + "signed_by_unicef_date": false, + "in_amendment": false, + "planned_budget": false, + "result_links": false, + "number": true, + "prc_review_attachment": false, + "activation_letter_attachment": false, "submission_date_prc": false, "id": false, + "unicef_focal_points": false, "special_reporting_requirements": false, - "country_programme": false, - "contingency_pd": false, - "termination_doc_attachment": false, - "unicef_signatory": false, + "reporting_periods": false, + "title": true, + "termination_doc": false, "frs": false, + "partner_authorized_officer_signatory": false, + "sections": false, + "signed_pd_document": false, + "unicef_signatory": false, + "termination_doc_attachment": false, + "signed_by_partner_date": false, + "planned_visits": false, + "actionpoint": false, "created": false, + "contingency_pd": false, + "partner_authorized_officer_signatory_id": false, + "prc_review_document": false, + "end": false, + "activity": false, + "country_programme": false, "country_programme_id": false, - "submission_date": false, - "planned_visits": false, - "termination_doc": false, - "flat_locations": false, - "unicef_focal_points": false, - "activation_letter": false, - "signed_by_unicef_date": false, - "reporting_periods": false, + "amendments": false, + "reference_number_year": true, + "partner_focal_points": false, + "unicef_signatory_id": false, "travel_activities": false, - "population_focus": false, + "metadata": false, + "modified": false, + "attachments": false, "agreement_id": false, - "activity": false, - "partner_focal_points": false, - "offices": false, - "signed_pd_document": false, - "document_type": true, - "number": true, - "engagement": false, + "flat_locations": false, + "reporting_requirements": false, + "review_date_prc": false, + "signed_pd_attachment": false, + "status": false, + "population_focus": false, "sections_present": false }, "view": { - "reporting_requirements": false, - "signed_pd_attachment": false, - "activation_letter_attachment": false, - "partner_authorized_officer_signatory_id": true, - "metadata": true, - "reference_number_year": true, - "prc_review_attachment": false, - "attachments": true, - "modified": true, - "status": true, - "result_links": true, - "planned_budget": false, - "review_date_prc": false, - "in_amendment": false, - "end": false, - "amendments": false, - "prc_review_document": false, - "unicef_signatory_id": true, - "agreement": false, - "actionpoint": true, "start": false, - "sections": false, - "partner_authorized_officer_signatory": false, - "signed_by_partner_date": false, - "title": false, + "document_type": false, + "activation_letter": false, + "offices": false, + "engagement": true, + "agreement": false, + "submission_date": false, + "signed_by_unicef_date": false, + "in_amendment": false, + "planned_budget": false, + "result_links": true, + "number": false, + "prc_review_attachment": false, + "activation_letter_attachment": false, "submission_date_prc": false, "id": true, + "unicef_focal_points": false, "special_reporting_requirements": true, - "country_programme": false, - "contingency_pd": false, - "termination_doc_attachment": true, - "unicef_signatory": false, + "reporting_periods": true, + "title": false, + "termination_doc": false, "frs": false, + "partner_authorized_officer_signatory": false, + "sections": false, + "signed_pd_document": false, + "unicef_signatory": false, + "termination_doc_attachment": true, + "signed_by_partner_date": false, + "planned_visits": false, + "actionpoint": true, "created": true, + "contingency_pd": false, + "partner_authorized_officer_signatory_id": true, + "prc_review_document": false, + "end": false, + "activity": true, + "country_programme": false, "country_programme_id": true, - "submission_date": false, - "planned_visits": false, - "termination_doc": false, - "flat_locations": false, - "unicef_focal_points": false, - "activation_letter": false, - "signed_by_unicef_date": false, - "reporting_periods": true, + "amendments": false, + "reference_number_year": true, + "partner_focal_points": false, + "unicef_signatory_id": true, "travel_activities": true, - "population_focus": true, + "metadata": true, + "modified": true, + "attachments": true, "agreement_id": true, - "activity": true, - "partner_focal_points": false, - "offices": false, - "signed_pd_document": false, - "document_type": false, - "number": false, - "engagement": true, + "flat_locations": false, + "reporting_requirements": false, + "review_date_prc": false, + "signed_pd_attachment": false, + "status": true, + "population_focus": true, "sections_present": false } }, - "partner_id": "572", + "partner_id": "1496", "sections": [], "planned_visits": [], "locations": [], @@ -258,7 +258,7 @@ "days_from_submission_to_signed": "Not fully signed", "days_from_review_to_signed": "Not Reviewed", "partner_vendor": null, - "reference_number_year": 2018, + "reference_number_year": 2019, "activation_letter_file": null, "activation_letter_attachment": null, "termination_doc_file": null, diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_amendments_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_amendments_.response.json index f323c35028..f5e618e5ba 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_amendments_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_amendments_.response.json @@ -19,25 +19,25 @@ ], "content-length": [ "Content-Length", - "646" + "666" ] }, "data": [ { - "id": 35, + "id": 42, "amendment_number": "1", - "signed_amendment_file": "http://testserver/media/test/file_attachments/partner_organization/573/573/agreements/600/interventions/508/amendments/None/test_file.pdf", + "signed_amendment_file": "http://testserver/media/test/file_attachments/partner_organization/1497/1497/agreements/1573/interventions/1455/amendments/None/test_file.pdf", "signed_amendment_attachment": null, "internal_prc_review": null, - "created": "2018-12-05T16:22:57.709000Z", - "modified": "2018-12-05T16:22:57.726000Z", + "created": "2019-07-01T20:32:43.465000Z", + "modified": "2019-07-01T20:32:43.501000Z", "types": [ - "Change IP name" + "Change authorized officer" ], - "other_description": "VJppZjLpmvqgiEiisnLUDfGzhjjeCDBpaJYLdRdZTNYwevzYtd", - "signed_date": "2018-12-05", - "signed_amendment": "http://testserver/media/test/file_attachments/partner_organization/573/573/agreements/600/interventions/508/amendments/None/test_file.pdf", - "intervention": 508 + "other_description": "pxRKBufTiSdqiGAOyXwghKOpNAMJCXkyyvpGCFZerSqsemMYaY", + "signed_date": "2019-07-01", + "signed_amendment": "http://testserver/media/test/file_attachments/partner_organization/1497/1497/agreements/1573/interventions/1455/amendments/None/test_file.pdf", + "intervention": 1455 } ], "content_type": null diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_applied-indicators_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_applied-indicators_.response.json new file mode 100644 index 0000000000..fe26545715 --- /dev/null +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_applied-indicators_.response.json @@ -0,0 +1,55 @@ +{ + "status_code": 200, + "headers": { + "content-type": [ + "Content-Type", + "application/json" + ], + "vary": [ + "Vary", + "Accept, Origin, Cookie" + ], + "allow": [ + "Allow", + "GET" + ], + "x-frame-options": [ + "X-Frame-Options", + "SAMEORIGIN" + ], + "content-length": [ + "Content-Length", + "458" + ] + }, + "data": [ + { + "id": 1456, + "title": "Intervention Title 2", + "number": "TST/PCA20191574/20191456", + "partner_name": "Partner 2", + "status": "draft", + "indicators": [], + "sections": [] + }, + { + "id": 1455, + "title": "Intervention Title 1", + "number": "TST/PCA20191573/20191455", + "partner_name": "Partner 1", + "status": "draft", + "indicators": [], + "sections": [] + }, + { + "id": 101, + "title": "Intervention Title 0", + "number": "TST/PCA20191572/2019101", + "partner_name": "Partner 0", + "status": "draft", + "indicators": [], + "sections": [] + } + ], + "content_type": null +} \ No newline at end of file diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_indicators_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_indicators_.response.json index bbccbb0bed..72bca7da1e 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_indicators_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_indicators_.response.json @@ -19,12 +19,12 @@ ], "content-length": [ "Content-Length", - "42" + "43" ] }, "data": [ { - "intervention": 509, + "intervention": 1456, "ram_indicators": [] } ], diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_map_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_map_.response.json index 8c7d907b24..7214c79013 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_map_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIIntervention/get__api_v2_interventions_map_.response.json @@ -19,18 +19,18 @@ ], "content-length": [ "Content-Length", - "918" + "931" ] }, "data": [ { - "id": 509, - "partner_id": "574", - "partner_name": "Partner 7", - "agreement": 601, + "id": 1456, + "partner_id": "1498", + "partner_name": "Partner 2", + "agreement": 1574, "document_type": "", - "number": "TST/PCA2018601/2018509", - "title": "Intervention Title 3", + "number": "TST/PCA20191574/20191456", + "title": "Intervention Title 2", "status": "draft", "start": null, "end": null, @@ -43,13 +43,13 @@ "frs": [] }, { - "id": 508, - "partner_id": "573", - "partner_name": "Partner 6", - "agreement": 600, + "id": 1455, + "partner_id": "1497", + "partner_name": "Partner 1", + "agreement": 1573, "document_type": "", - "number": "TST/PCA2018600/2018508", - "title": "Intervention Title 2", + "number": "TST/PCA20191573/20191455", + "title": "Intervention Title 1", "status": "draft", "start": null, "end": null, @@ -63,12 +63,12 @@ }, { "id": 101, - "partner_id": "572", - "partner_name": "Partner 5", - "agreement": 599, + "partner_id": "1496", + "partner_name": "Partner 0", + "agreement": 1572, "document_type": "", - "number": "TST/PCA2018599/2018101", - "title": "Intervention Title 1", + "number": "TST/PCA20191572/2019101", + "title": "Intervention Title 0", "status": "draft", "start": null, "end": null, diff --git a/src/etools/applications/partners/tests/test_api.py b/src/etools/applications/partners/tests/test_api.py index d04a7b582d..b5e20798b6 100644 --- a/src/etools/applications/partners/tests/test_api.py +++ b/src/etools/applications/partners/tests/test_api.py @@ -45,6 +45,8 @@ class TestAPIIntervention(BaseTenantTestCase, metaclass=ViewSetChecker): reverse("partners_api:intervention-indicators"), reverse("partners_api:intervention-amendments"), reverse("partners_api:intervention-map"), + reverse("partners_api:intervention-applied-indicators-list"), + ] def get_fixtures(cls): diff --git a/src/etools/applications/partners/urls_v2.py b/src/etools/applications/partners/urls_v2.py index 7d01c674a4..05a15cb3d5 100644 --- a/src/etools/applications/partners/urls_v2.py +++ b/src/etools/applications/partners/urls_v2.py @@ -34,6 +34,7 @@ InterventionResultLinkListCreateView, InterventionResultLinkUpdateView, InterventionResultListAPIView, + InterventionWithAppliedIndicatorsView, ) from etools.applications.partners.views.partner_organization_v2 import ( PartnerNotAssuranceCompliant, @@ -135,6 +136,10 @@ view=InterventionListAPIView.as_view(http_method_names=['get', 'post']), name='intervention-list'), + url(r'^interventions/applied-indicators/$', + view=InterventionWithAppliedIndicatorsView.as_view(http_method_names=['get', ]), + name='intervention-applied-indicators-list'), + url(r'^interventions/result-links/(?P\d+)/lower-results/$', view=InterventionLowerResultListCreateView.as_view(http_method_names=['get', 'post']), name='intervention-lower-results-list'), diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index aa77dd8a3b..9ae4f2b0c8 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -51,6 +51,7 @@ InterventionResultExportSerializer, ) from etools.applications.partners.serializers.interventions_v2 import ( + BasicInterventionListSerializer, InterventionAmendmentCUSerializer, InterventionAttachmentSerializer, InterventionBudgetCUSerializer, @@ -226,6 +227,22 @@ def list(self, request): return response +class InterventionWithAppliedIndicatorsView(QueryStringFilterMixin, ListAPIView): + """ Interventions.""" + queryset = Intervention.objects.all() + serializer_class = BasicInterventionListSerializer + permission_classes = (PartnershipManagerPermission,) + + filters = ( + ('sections', 'sections__in'), + ('status', 'status__in'), + ) + + def get_queryset(self): + return super().get_queryset().select_related('agreement__partner').prefetch_related( + 'result_links__ll_results__applied_indicators__indicator') + + class InterventionListDashView(InterventionListBaseView): """ Create new Interventions. diff --git a/src/etools/applications/reports/migrations/0018_section_active.py b/src/etools/applications/reports/migrations/0018_section_active.py new file mode 100644 index 0000000000..57a677270d --- /dev/null +++ b/src/etools/applications/reports/migrations/0018_section_active.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.3 on 2019-07-09 19:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0017_auto_20190424_1509'), + ] + + operations = [ + migrations.AddField( + model_name='section', + name='active', + field=models.BooleanField(default=True, verbose_name='Active'), + ), + ] diff --git a/src/etools/applications/reports/models.py b/src/etools/applications/reports/models.py index 927bff6c7f..e3e15c5941 100644 --- a/src/etools/applications/reports/models.py +++ b/src/etools/applications/reports/models.py @@ -140,6 +140,7 @@ class Section(TimeStampedModel): alternate_name = models.CharField(max_length=255, null=True, default='', verbose_name=_('Alternate Name')) dashboard = models.BooleanField(default=False, verbose_name=_('Dashboard')) color = models.CharField(max_length=7, null=True, blank=True, verbose_name=_('Color')) + active = models.BooleanField(default=True, verbose_name=_('Active')) class Meta: ordering = ['name'] diff --git a/src/etools/applications/reports/serializers/v2.py b/src/etools/applications/reports/serializers/v2.py index 95a74cd39c..410294270f 100644 --- a/src/etools/applications/reports/serializers/v2.py +++ b/src/etools/applications/reports/serializers/v2.py @@ -224,6 +224,14 @@ class Meta: fields = '__all__' +class AppliedIndicatorBasicSerializer(serializers.ModelSerializer): + title = serializers.ReadOnlyField(source='indicator.title') + + class Meta: + model = AppliedIndicator + fields = ('pk', 'title', 'section') + + class ClusterSerializer(serializers.ModelSerializer): class Meta: diff --git a/src/etools/applications/t2f/filters/__init__.py b/src/etools/applications/t2f/filters/__init__.py index 535445c10b..eb6baa4e9f 100644 --- a/src/etools/applications/t2f/filters/__init__.py +++ b/src/etools/applications/t2f/filters/__init__.py @@ -1,4 +1,3 @@ - from django.db.models import F from django.db.models.query_utils import Q @@ -80,6 +79,12 @@ def filter_queryset(self, request, queryset, view): if statuses is not None: statuses = statuses.split(',') queryset = queryset.filter(status__in=statuses) + + mf_sections = request.query_params.get('mf_sections', None) + if mf_sections is not None: + mf_sections = mf_sections.split(',') + queryset = queryset.filter(section__in=mf_sections) + # To have proper keys in data dict, the serializer renames the incoming values according to the needs return queryset.filter(**data) diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index 43818cd5cf..83d17a9c8a 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -476,15 +476,6 @@ def before_send(event, hint): 'etools.applications.core.auth.DRFBasicAuthMixin', ) -ISSUE_CHECKS = [ - 'etools.applications.management.issues.project_checks.ActivePCANoSignedDocCheck', - 'etools.applications.management.issues.project_checks.PdOutputsWrongCheck', - 'etools.applications.management.issues.project_checks.InterventionsAssociatedSSFACheck', - 'etools.applications.management.issues.project_checks.InterventionsAreValidCheck', - 'etools.applications.management.issues.project_checks.PDAmendmentsMissingFilesCheck', - 'etools.applications.management.issues.project_checks.PCAAmendmentsMissingFilesCheck', -] - EMAIL_FOR_USER_RESPONSIBLE_FOR_INVESTIGATION_ESCALATIONS = get_from_secrets_or_env( 'EMAIL_FOR_USER_RESPONSIBLE_FOR_INVESTIGATION_ESCALATIONS', 'integrity1@unicef.org' ) From c8f681bb6e10ec4e01036eed0f45d343a4fef252 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 5 Aug 2019 15:01:16 -0400 Subject: [PATCH 18/56] amend --- .../applications/management/handlers.py | 44 +++++++++++++------ .../management/tests/test_handlers.py | 8 ++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/etools/applications/management/handlers.py b/src/etools/applications/management/handlers.py index 052ee23182..39e76725f4 100644 --- a/src/etools/applications/management/handlers.py +++ b/src/etools/applications/management/handlers.py @@ -13,7 +13,7 @@ class MigrationException(Exception): class NotDeactivatedException(MigrationException): - """Exception thrown when deactivated is still referenced""" + """Exception thrown when an active objects is still referenced by a inactive section""" class IndicatorSectionInconsistentException(MigrationException): @@ -47,8 +47,8 @@ class SectionHandler: TPMVisit.REPORT_REJECTED ] - # dictionary with info related to the model, active queryset (object relevant to the migration) - # attribute name and type of relation + # dictionary to mark instances, for each model that has a m2m relationship to Sections, + # in order to follow up later and clean (remove references to old sections) them. queryset_migration_mapping = { 'interventions': ( Intervention.objects.filter(status__in=intervention_updatable_status), @@ -91,17 +91,16 @@ def new(new_section_name): @staticmethod @atomic def merge(new_section_name, sections_to_merge): - """Create two or more sections into a new section migrating active objects""" + """Merge two or more sections into a newly create section and migrating active objects""" from_instances = Section.objects.filter(pk__in=sections_to_merge) to_instance = Section.objects.create(name=new_section_name) from_instances.update(active=False) # m2m relation need to be cleaned at the end m2m_to_clean = { - 'interventions': [] } for from_instance in from_instances: - SectionHandler.__update_objects(from_instance, to_instance, m2m_to_clean) + m2m_to_clean = SectionHandler.__update_objects(from_instance, to_instance, m2m_to_clean) SectionHandler.__clean_m2m(from_instances, m2m_to_clean) # history @@ -124,15 +123,13 @@ def close(from_instance_pk, new_section_2_new_querysets): from_instance.save() new_sections = [] - # m2m relation need to be cleaned at the end - m2m_to_clean = { - 'interventions': [] - } - + # m2m relation need to be cleaned at the end. + # It's a dictionary to mark instances for each model in a Many to Many Relationshipm that we follow up for cleaning at the end of + m2m_to_clean = {} for new_section_name, queryset_mapping_dict in new_section_2_new_querysets.items(): to_instance, _ = Section.objects.get_or_create(name=new_section_name) new_sections.append(to_instance) - SectionHandler.__update_objects(from_instance, to_instance, m2m_to_clean, queryset_mapping_dict) + m2m_to_clean = SectionHandler.__update_objects(from_instance, to_instance, m2m_to_clean, queryset_mapping_dict) SectionHandler.__clean_m2m([from_instance], m2m_to_clean) @@ -149,8 +146,24 @@ def close(from_instance_pk, new_section_2_new_querysets): @staticmethod def __update_objects(from_instance, to_instance, m2m_to_clean, section_split_dict=None): """ - update eTools queryset from one instance to another - section_split_dict has the redistribution mapping after a section is closed + It updates sections (from from_instance to to_instance) for all instances of all models defined in + queryset_migration_mapping as keys, following the filters defined in that mapping. + + Params: + from_instance: Section, instance of section to map from. + to_instance: Section, instance of Section model that is desired to map all instances of models defined in + queryset_migration_mapping + + m2m_to_clean: dict, key: model, value: list of pk of objects marked for removing the to_instance Section + + section_split_dict: dict, key: name of new section, value dict with model and pk of instances to migrate + "Nutrition": { + 'interventions': [1, 3], + 'applied_indicators': [1], + 'travels': [2, 4 ], + 'tpm_activities': [], + 'action_points': [3], + }, """ for model_key in SectionHandler.queryset_migration_mapping.keys(): qs, section_attribute, relation = SectionHandler.queryset_migration_mapping[model_key] @@ -161,10 +174,13 @@ def __update_objects(from_instance, to_instance, m2m_to_clean, section_split_dic if relation == SectionHandler.M2M: to_update = SectionHandler.__update_m2m(qs, section_attribute, from_instance, to_instance) + if model_key not in m2m_to_clean: + m2m_to_clean[model_key] = [] m2m_to_clean[model_key].extend(to_update) elif relation == SectionHandler.FK: SectionHandler.__update_fk(qs, section_attribute, from_instance, to_instance) + return m2m_to_clean @staticmethod def __update_fk(qs, section_attribute, old_section, new_section): diff --git a/src/etools/applications/management/tests/test_handlers.py b/src/etools/applications/management/tests/test_handlers.py index 632d1ec11a..b6b644dc52 100644 --- a/src/etools/applications/management/tests/test_handlers.py +++ b/src/etools/applications/management/tests/test_handlers.py @@ -91,11 +91,11 @@ def test_merge(self): SectionHandler.merge('Health and Nutrition', [self.health.pk, self.nutrition.pk]) - h_n = Section.objects.get(name='Health and Nutrition') + health_and_nutrition = Section.objects.get(name='Health and Nutrition') self.__check_history(4, 2, 1, SectionHistory.MERGE, 2, 1) self.__check_section(self.health, (1, 1, 1, 0)) self.__check_section(self.nutrition, (1, 0, 1, 1)) - self.__check_section(h_n, (2, 3, 2, 1)) + self.__check_section(health_and_nutrition, (2, 3, 2, 1)) self.__check_section(self.ict, (1, 1, 1, 0)) def test_close_ok(self): @@ -157,14 +157,14 @@ def test_close_fail_inconsistent_section(self): self.__check_history(3, 0, 0, None, None, None) self.__check_before() close_dict = { - "Hea": { + "Health 1": { 'interventions': [self.intervention1.pk, self.intervention3.pk], 'applied_indicators': [self.applied_indicator1.pk], 'travels': [self.travel2.pk, ], 'tpm_activities': [], 'action_points': [self.action_point.pk], }, - "Lth": { + "Health 2": { 'interventions': [self.intervention1.pk], 'applied_indicators': [self.applied_indicator5.pk], 'travels': [self.travel1.pk, self.travel2.pk], From efdacafcd2cd48a297de34356b2cad0a71e4de97 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 6 Aug 2019 10:01:38 -0400 Subject: [PATCH 19/56] amend --- src/etools/applications/management/handlers.py | 2 +- .../migrations/0005_auto_20190806_1400.py | 18 ++++++++++++++++++ src/etools/applications/management/models.py | 2 -- .../applications/management/views/sections.py | 6 +++--- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 src/etools/applications/management/migrations/0005_auto_20190806_1400.py diff --git a/src/etools/applications/management/handlers.py b/src/etools/applications/management/handlers.py index 39e76725f4..170426e015 100644 --- a/src/etools/applications/management/handlers.py +++ b/src/etools/applications/management/handlers.py @@ -169,7 +169,7 @@ def __update_objects(from_instance, to_instance, m2m_to_clean, section_split_dic qs, section_attribute, relation = SectionHandler.queryset_migration_mapping[model_key] if section_split_dict: # if it's a close we filter the queryset - instance_pks = section_split_dict[model_key] if section_split_dict else [] + instance_pks = section_split_dict[model_key] if section_split_dict and model_key in section_split_dict else [] qs = qs.filter(pk__in=instance_pks) if relation == SectionHandler.M2M: diff --git a/src/etools/applications/management/migrations/0005_auto_20190806_1400.py b/src/etools/applications/management/migrations/0005_auto_20190806_1400.py new file mode 100644 index 0000000000..84d0e15bd8 --- /dev/null +++ b/src/etools/applications/management/migrations/0005_auto_20190806_1400.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-08-06 14:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0004_auto_20190715_2047'), + ] + + operations = [ + migrations.AlterField( + model_name='sectionhistory', + name='history_type', + field=models.CharField(choices=[('create', 'Create'), ('merge', 'Merge'), ('close', 'Close')], max_length=10, verbose_name='Name'), + ), + ] diff --git a/src/etools/applications/management/models.py b/src/etools/applications/management/models.py index ccb6c9602e..0ec8e0352e 100644 --- a/src/etools/applications/management/models.py +++ b/src/etools/applications/management/models.py @@ -10,13 +10,11 @@ class SectionHistory(TimeStampedModel): """ Model to keep history of Section changes""" CREATE = 'create' MERGE = 'merge' - SPLIT = 'split' CLOSE = 'close' TYPES = ( (CREATE, 'Create'), (MERGE, 'Merge'), - (SPLIT, 'Split'), (CLOSE, 'Close'), ) diff --git a/src/etools/applications/management/views/sections.py b/src/etools/applications/management/views/sections.py index d4d3dc4506..84beee75ae 100644 --- a/src/etools/applications/management/views/sections.py +++ b/src/etools/applications/management/views/sections.py @@ -28,7 +28,7 @@ def new(self, request): return Response(f'Section {new_section_name} already exist', status=status.HTTP_400_BAD_REQUEST) return Response({ - 'pk': section.pk, + 'id': section.id, 'name': section.name, }) @@ -46,7 +46,7 @@ def merge(self, request): return Response(f'Section {new_section_name} already exist', status=status.HTTP_400_BAD_REQUEST) return Response({ - 'pk': section.pk, + 'id': section.id, 'name': section.name, }) @@ -61,5 +61,5 @@ def close(self, request): return Response(_('Unable to unpack'), status=status.HTTP_400_BAD_REQUEST) sections = SectionHandler.close(old_section, objects_dict) - data = [{'pk': section.pk, 'name': section.name} for section in sections] + data = [{'id': section.id, 'name': section.name} for section in sections] return Response(data) From 18888e8a489eb7a92dd89a9f793d64ee980d756f Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 6 Aug 2019 13:35:17 -0400 Subject: [PATCH 20/56] tweaks --- src/etools/applications/management/handlers.py | 6 ++++-- src/etools/applications/reports/admin.py | 2 +- src/etools/applications/reports/views/v1.py | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/etools/applications/management/handlers.py b/src/etools/applications/management/handlers.py index 170426e015..443c86c96c 100644 --- a/src/etools/applications/management/handlers.py +++ b/src/etools/applications/management/handlers.py @@ -213,8 +213,10 @@ def __clean_m2m(from_instances, m2m_to_clean): def __disabled_section_check(old_section): """Checks that the old section is not referenced by any active objects""" for _, (qs, attribute_name, _) in SectionHandler.queryset_migration_mapping.items(): - if qs.filter(**{attribute_name: old_section}).exists(): - raise NotDeactivatedException(f'{qs.model.__name__} in active status exists') + active_references = qs.filter(**{attribute_name: old_section}) + if active_references.exists(): + pks = ' '.join(str(pk) for pk in active_references.values_list('pk', flat=True)) + raise NotDeactivatedException(f'{qs.model.__name__} in active status exists {pks}') @staticmethod def __consistent_indicators_check(new_sections): diff --git a/src/etools/applications/reports/admin.py b/src/etools/applications/reports/admin.py index 3f13d07c53..d507782c2d 100644 --- a/src/etools/applications/reports/admin.py +++ b/src/etools/applications/reports/admin.py @@ -52,7 +52,7 @@ def queryset(self, request, queryset): class SectionAdmin(admin.ModelAdmin): form = AutoSizeTextForm - list_display = ('name', 'color', 'dashboard',) + list_display = ('name', 'color', 'dashboard', 'active', ) list_editable = ('color', 'dashboard',) diff --git a/src/etools/applications/reports/views/v1.py b/src/etools/applications/reports/views/v1.py index 2f81c03bea..2cb3663438 100644 --- a/src/etools/applications/reports/views/v1.py +++ b/src/etools/applications/reports/views/v1.py @@ -3,6 +3,7 @@ from rest_framework.generics import ListAPIView, RetrieveAPIView from rest_framework.permissions import IsAdminUser from rest_framework.response import Response +from unicef_restlib.views import QueryStringFilterMixin from etools.applications.reports.models import CountryProgramme, Indicator, Result, ResultType, Section, Unit from etools.applications.reports.serializers.v1 import ( @@ -25,7 +26,8 @@ class ResultTypeViewSet(mixins.ListModelMixin, serializer_class = ResultTypeSerializer -class SectionViewSet(mixins.RetrieveModelMixin, +class SectionViewSet(QueryStringFilterMixin, + mixins.RetrieveModelMixin, mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): @@ -34,7 +36,9 @@ class SectionViewSet(mixins.RetrieveModelMixin, """ queryset = Section.objects.all() serializer_class = SectionCreateSerializer - + filters = ( + ('active', 'active'), + ) class ResultViewSet(viewsets.ModelViewSet): """ From c74a71a94004a808015ced2a2758386bc69799c1 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 6 Aug 2019 14:18:49 -0400 Subject: [PATCH 21/56] flake8 --- src/etools/applications/reports/views/v1.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/applications/reports/views/v1.py b/src/etools/applications/reports/views/v1.py index 2cb3663438..5c09420e8b 100644 --- a/src/etools/applications/reports/views/v1.py +++ b/src/etools/applications/reports/views/v1.py @@ -40,6 +40,7 @@ class SectionViewSet(QueryStringFilterMixin, ('active', 'active'), ) + class ResultViewSet(viewsets.ModelViewSet): """ Returns a list of all Results From cf5046cdabd01b23f9139adfa54268723f2bc431 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 6 Aug 2019 14:33:55 -0400 Subject: [PATCH 22/56] amends --- .../applications/management/handlers.py | 16 ++++++++++++ .../management/migrations_issues.py | 26 ------------------- 2 files changed, 16 insertions(+), 26 deletions(-) delete mode 100644 src/etools/applications/management/migrations_issues.py diff --git a/src/etools/applications/management/handlers.py b/src/etools/applications/management/handlers.py index 443c86c96c..6967c2e369 100644 --- a/src/etools/applications/management/handlers.py +++ b/src/etools/applications/management/handlers.py @@ -116,6 +116,22 @@ def close(from_instance_pk, new_section_2_new_querysets): """ Close a section and split the active objects according the mapping dictionary new_section_2_new_querysets has a mapping of the instances to be mapped to new section + { + "Nutrition": { + 'interventions': [1, 3], + 'applied_indicators': [1], + 'travels': [2, 4 ], + 'tpm_activities': [], + 'action_points': [3], + }, + "Health": { + 'interventions': [1, 5], + 'applied_indicators': [2], + 'travels': [4 ], + 'tpm_activities': [], + 'action_points': [1], + } + } """ from_instance = Section.objects.get(pk=from_instance_pk) diff --git a/src/etools/applications/management/migrations_issues.py b/src/etools/applications/management/migrations_issues.py deleted file mode 100644 index 50b5edb7b8..0000000000 --- a/src/etools/applications/management/migrations_issues.py +++ /dev/null @@ -1,26 +0,0 @@ -from etools.applications.core.util_scripts import set_country -from etools.applications.reports.models import Indicator, Result -from etools.applications.users.models import Country - -model = Result -for workspace in Country.objects.exclude(schema_name='public'): - set_country(workspace.name) - if model.objects.filter(sector__isnull=False).exists(): - print(model.objects.filter(sector__isnull=False).count(), workspace) - -# 5 China -# 21 FRG -# 2 Sierra Leone -# 55 UAT - -model = Indicator -for workspace in Country.objects.exclude(schema_name='public'): - set_country(workspace.name) - if model.objects.filter(sector__isnull=False).exists(): - print(model.objects.filter(sector__isnull=False).count(), workspace) - -# 2 Cambodia -# 1 China -# 8 FRG -# 16 Sierra Leone -# 27 UAT From e574a12599b5dd2908e8323abe84b0d93d9f2694 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 6 Aug 2019 14:42:58 -0400 Subject: [PATCH 23/56] a --- src/etools/applications/management/views/sections.py | 8 ++++---- .../applications/partners/serializers/interventions_v2.py | 2 +- .../applications/partners/views/interventions_v2.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/etools/applications/management/views/sections.py b/src/etools/applications/management/views/sections.py index 84beee75ae..c9ac892095 100644 --- a/src/etools/applications/management/views/sections.py +++ b/src/etools/applications/management/views/sections.py @@ -24,8 +24,8 @@ def new(self, request): try: section = SectionHandler.new(new_section_name) - except IntegrityError: - return Response(f'Section {new_section_name} already exist', status=status.HTTP_400_BAD_REQUEST) + except IntegrityError as e: + return Response(str(e), status=status.HTTP_400_BAD_REQUEST) return Response({ 'id': section.id, @@ -42,8 +42,8 @@ def merge(self, request): return Response(_('Unable to unpack'), status=status.HTTP_400_BAD_REQUEST) try: section = SectionHandler.merge(new_section_name, sections_to_merge) - except IntegrityError: - return Response(f'Section {new_section_name} already exist', status=status.HTTP_400_BAD_REQUEST) + except IntegrityError as e: + return Response(str(e), status=status.HTTP_400_BAD_REQUEST) return Response({ 'id': section.id, diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 09d9fcee0f..598a5f9fbc 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -266,7 +266,7 @@ class Meta: ) -class BasicInterventionListSerializer(serializers.ModelSerializer): +class InterventionToIndicatorsListSerializer(serializers.ModelSerializer): partner_name = serializers.CharField(source='agreement.partner.name') indicators = serializers.SerializerMethodField() diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index 9ae4f2b0c8..362a3a195e 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -51,7 +51,6 @@ InterventionResultExportSerializer, ) from etools.applications.partners.serializers.interventions_v2 import ( - BasicInterventionListSerializer, InterventionAmendmentCUSerializer, InterventionAttachmentSerializer, InterventionBudgetCUSerializer, @@ -68,6 +67,7 @@ InterventionResultCUSerializer, InterventionResultLinkSimpleCUSerializer, InterventionResultSerializer, + InterventionToIndicatorsListSerializer, MinimalInterventionListSerializer, PlannedVisitsCUSerializer, ) @@ -230,7 +230,7 @@ def list(self, request): class InterventionWithAppliedIndicatorsView(QueryStringFilterMixin, ListAPIView): """ Interventions.""" queryset = Intervention.objects.all() - serializer_class = BasicInterventionListSerializer + serializer_class = InterventionToIndicatorsListSerializer permission_classes = (PartnershipManagerPermission,) filters = ( From d5bf7c1f40208283354f4f7d9d8f6966a723e7b2 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 13 Aug 2019 11:29:56 -0400 Subject: [PATCH 24/56] 13710 vision sync exceptions --- src/etools/applications/funds/synchronizers.py | 4 ++-- src/etools/applications/funds/views.py | 4 ++-- src/etools/applications/vision/exceptions.py | 5 ----- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 src/etools/applications/vision/exceptions.py diff --git a/src/etools/applications/funds/synchronizers.py b/src/etools/applications/funds/synchronizers.py index 65f80478ef..87ffa63528 100644 --- a/src/etools/applications/funds/synchronizers.py +++ b/src/etools/applications/funds/synchronizers.py @@ -5,6 +5,7 @@ from django.db.models import Sum +from unicef_vision.exceptions import VisionException from unicef_vision.synchronizers import FileDataSynchronizer, ManualVisionSynchronizer from unicef_vision.utils import comp_decimals @@ -14,7 +15,6 @@ FundsReservationHeader, FundsReservationItem, ) -from etools.applications.vision.exceptions import VisionSyncException from etools.applications.vision.synchronizers import VisionDataTenantSynchronizer @@ -124,7 +124,7 @@ def _convert_records(self, records): json_records = json.loads(records)["ROWSET"]["ROW"] except json.decoder.JSONDecodeError: if "No data exists" in records: - raise VisionSyncException("Vision error 400: No data could be found") + raise VisionException("Vision error 400: No data could be found") else: raise # Since our counterpart APIs are designed in a way that can surprise us what data structure we might encounter diff --git a/src/etools/applications/funds/views.py b/src/etools/applications/funds/views.py index fc925d9788..7d1f715dad 100644 --- a/src/etools/applications/funds/views.py +++ b/src/etools/applications/funds/views.py @@ -8,6 +8,7 @@ from rest_framework.views import APIView from rest_framework_csv.renderers import CSVRenderer, JSONRenderer from unicef_restlib.views import QueryStringFilterMixin +from unicef_vision.exceptions import VisionException from etools.applications.core.mixins import ExportModelMixin from etools.applications.core.renderers import CSVFlatRenderer @@ -42,7 +43,6 @@ from etools.applications.partners.filters import PartnerScopeFilter from etools.applications.partners.models import Intervention from etools.applications.partners.permissions import PartnershipManagerPermission -from etools.applications.vision.exceptions import VisionSyncException class FRsView(APIView): @@ -74,7 +74,7 @@ def get(self, request, format=None): # try to get this fr from vision try: sync_single_delegated_fr(request.user.profile.country.business_area_code, delegated_fr) - except VisionSyncException as e: + except VisionException as e: return self.bad_request('The FR {} could not be found in eTools and could not be synced ' 'from Vision. {}'.format(delegated_fr, e)) qs._result_cache = None diff --git a/src/etools/applications/vision/exceptions.py b/src/etools/applications/vision/exceptions.py deleted file mode 100644 index 6cf47cfdaa..0000000000 --- a/src/etools/applications/vision/exceptions.py +++ /dev/null @@ -1,5 +0,0 @@ - - -class VisionSyncException(Exception): - def __init__(self, message): - self.message = message From 7477211ba26d2e28623ae0617db8a0e062e8b737 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 13 Aug 2019 12:54:06 -0400 Subject: [PATCH 25/56] tweaks --- .../applications/funds/tests/test_views.py | 23 +++++++++++++------ .../fund_reservation_invalid.yml | 2 +- src/etools/applications/funds/views.py | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/etools/applications/funds/tests/test_views.py b/src/etools/applications/funds/tests/test_views.py index 90e06803a1..9c48b53492 100644 --- a/src/etools/applications/funds/tests/test_views.py +++ b/src/etools/applications/funds/tests/test_views.py @@ -6,7 +6,6 @@ from django.utils import timezone from rest_framework import status -from unicef_vision.exceptions import VisionException from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.funds.tests.factories import ( @@ -112,15 +111,25 @@ def test_get_fail_with_no_values(self): @VCR.use_cassette(str(Path(__file__).parent / 'vcr_cassettes/fund_reservation_invalid.yml')) def test_get_fail_with_non_existant_values(self): - data = {'values': ','.join(['im a bad value', 'another bad value'])} - with self.assertRaises(VisionException): - status_code, result = self.run_request(data) - self.assertEqual(status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(result['error'], 'One or more of the FRs are used by another PD/SSFA ' - 'or could not be found in eTools.') + data = {'values': ','.join(['another bad value', 'im a bad value', ])} + status_code, result = self.run_request(data) + self.assertEqual(status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result['error'], 'The FR another bad value could not be found in eTools and could not be ' + 'synced from Vision. Vision error 400: No data could be found') # TODO: add tests to cover, frs correctly brought in from mock. with correct vendor numbers, FR missing from vision, # FR with multiple line items, and FR with only one line item. + @VCR.use_cassette(str(Path(__file__).parent / 'vcr_cassettes/fund_reservation.yml')) + def test_get_fail_with_already_used_fr(self): + new_intervention = InterventionFactory() + self.fr_1.intervention = self.intervention + self.fr_1.save() + data = {'values': ','.join(['9999', self.fr_1.fr_number]), + 'intervention': new_intervention.pk} + status_code, result = self.run_request(data) + self.assertEqual(status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result['error'], f'FR #{self.fr_1} is already being used by PD/SSFA ref [{self.intervention}]') + @VCR.use_cassette(str(Path(__file__).parent / 'vcr_cassettes/fund_reservation.yml')) def test_get_success_sync_vision(self): self.fr_1.intervention = self.intervention diff --git a/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation_invalid.yml b/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation_invalid.yml index ccad3bf237..939ccc1c3a 100644 --- a/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation_invalid.yml +++ b/src/etools/applications/funds/tests/vcr_cassettes/fund_reservation_invalid.yml @@ -22,7 +22,7 @@ interactions: Content-Type: - application/json; charset=utf-8 Date: - - Wed, 19 Jun 2019 20:09:02 GMT + - Tue, 13 Aug 2019 17:44:46 GMT Server: - Microsoft-IIS/8.5 Microsoft-HTTPAPI/2.0 Vary: diff --git a/src/etools/applications/funds/views.py b/src/etools/applications/funds/views.py index 7d1f715dad..74cea96f45 100644 --- a/src/etools/applications/funds/views.py +++ b/src/etools/applications/funds/views.py @@ -63,12 +63,11 @@ def get(self, request, format=None): if len(values) > len(set(values)): return self.bad_request('You have duplicate records of the same FR, please make sure to add' ' each FR only one time') - qs = FundsReservationHeader.objects.filter(fr_number__in=values) - not_found = set(values) - set(qs.values_list('fr_number', flat=True)) if not_found: nf = list(not_found) + nf.sort() with transaction.atomic(): for delegated_fr in nf: # try to get this fr from vision @@ -77,6 +76,7 @@ def get(self, request, format=None): except VisionException as e: return self.bad_request('The FR {} could not be found in eTools and could not be synced ' 'from Vision. {}'.format(delegated_fr, e)) + qs._result_cache = None if intervention_id: From 9302d64a51165230bed05d98845ca774e1b7ce41 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Mon, 5 Aug 2019 22:08:02 +0300 Subject: [PATCH 26/56] fix partner import keyerror when 'partner_type_desc' is missing --- src/etools/applications/partners/synchronizers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/partners/synchronizers.py b/src/etools/applications/partners/synchronizers.py index 3db756ab2e..95529f4901 100644 --- a/src/etools/applications/partners/synchronizers.py +++ b/src/etools/applications/partners/synchronizers.py @@ -260,7 +260,8 @@ def get_partner_type(partner): 'GOVERNMENT': 'Government', 'UN AGENCY': 'UN Agency', } - return type_mapping.get(partner['PARTNER_TYPE_DESC'].upper(), None) + if 'PARTNER_TYPE_DESC' in partner: + return type_mapping.get(partner['PARTNER_TYPE_DESC'].upper(), None) @staticmethod def get_partner_rating(partner): From c3f4e2596716c462edaaddbcb9699396e504d8e0 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Thu, 8 Aug 2019 23:14:51 +0300 Subject: [PATCH 27/56] remove keycheck for the required field 'PARTNER_TYPE_DESC', add a more generic check in the view --- src/etools/applications/partners/synchronizers.py | 2 -- .../applications/partners/views/partner_organization_v2.py | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/synchronizers.py b/src/etools/applications/partners/synchronizers.py index 95529f4901..ce401a5738 100644 --- a/src/etools/applications/partners/synchronizers.py +++ b/src/etools/applications/partners/synchronizers.py @@ -260,8 +260,6 @@ def get_partner_type(partner): 'GOVERNMENT': 'Government', 'UN AGENCY': 'UN Agency', } - if 'PARTNER_TYPE_DESC' in partner: - return type_mapping.get(partner['PARTNER_TYPE_DESC'].upper(), None) @staticmethod def get_partner_rating(partner): diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index 822c337eb5..cc003991fa 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -471,6 +471,12 @@ def create(self, request, *args, **kwargs): country = request.user.profile.country partner_sync = PartnerSynchronizer(business_area_code=country.business_area_code) + + partner_resp = partner_sync._filter_records([partner_resp]).pop() + if not partner_resp: + return Response({"error": 'Partner skipped because one or more of the required fields are missing'}, + status=status.HTTP_400_BAD_REQUEST) + partner_sync._partner_save(partner_resp, full_sync=False) partner = PartnerOrganization.objects.get( From e874219320467b51388e0306597a8bf060c42e46 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Thu, 8 Aug 2019 23:21:28 +0300 Subject: [PATCH 28/56] fix mistake --- src/etools/applications/partners/synchronizers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/applications/partners/synchronizers.py b/src/etools/applications/partners/synchronizers.py index ce401a5738..3db756ab2e 100644 --- a/src/etools/applications/partners/synchronizers.py +++ b/src/etools/applications/partners/synchronizers.py @@ -260,6 +260,7 @@ def get_partner_type(partner): 'GOVERNMENT': 'Government', 'UN AGENCY': 'UN Agency', } + return type_mapping.get(partner['PARTNER_TYPE_DESC'].upper(), None) @staticmethod def get_partner_rating(partner): From b1aa6e497d2b27818647391e55b4f735bbe8ba75 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Thu, 8 Aug 2019 23:47:07 +0300 Subject: [PATCH 29/56] update tests --- .../partners/tests/test_api_partners.py | 63 ++++++++++++++++++- .../partners/views/partner_organization_v2.py | 3 +- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/tests/test_api_partners.py b/src/etools/applications/partners/tests/test_api_partners.py index 3d32f53d02..a07dd3c4fc 100644 --- a/src/etools/applications/partners/tests/test_api_partners.py +++ b/src/etools/applications/partners/tests/test_api_partners.py @@ -473,6 +473,63 @@ def test_vendor_exists(self): {"error": "This vendor number already exists in eTools"} ) + def test_missing_required_keys(self): + mock_insight = Mock(return_value=(True, { + "ROWSET": { + "ROW": { + "VENDOR_CODE": "321", + } + } + })) + with patch(INSIGHT_PATH, mock_insight): + response = self.forced_auth_req( + 'post', + "{}?vendor=321".format(self.url), + data={}, + view=self.view + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data, + {"error": "Partner skipped because one or more of the required fields are missing"} + ) + + def test_invalid_partner_type(self): + vendor_number = "321" + mock_insight = Mock(return_value=(True, { + "ROWSET": { + "ROW": { + "PARTNER_TYPE_DESC": "SOMETHING INVALID", + "VENDOR_CODE": vendor_number, + "VENDOR_NAME": "New Partner", + "CSO_TYPE": "National NGO", + "CORE_VALUE_ASSESSMENT_DT": "01-Jan-01", + 'TOTAL_CASH_TRANSFERRED_CP': "2,000", + 'TOTAL_CASH_TRANSFERRED_CY': "2,000", + 'NET_CASH_TRANSFERRED_CY': "2,000", + 'TOTAL_CASH_TRANSFERRED_YTD': "2,000", + 'REPORTED_CY': "2,000", + "COUNTRY": "239", + "MARKED_FOR_DELETION": 'true', + "POSTING_BLOCK": 'true', + } + } + })) + with patch(INSIGHT_PATH, mock_insight): + response = self.forced_auth_req( + 'post', + "{}?vendor=321".format(self.url), + data={}, + view=self.view + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + qs = PartnerOrganization.objects.filter(vendor_number=vendor_number) + self.assertTrue(qs.exists()) + partner = qs.first() + self.assertEqual(partner.hidden, True) + self.assertEqual(partner.blocked, True) + self.assertEqual(partner.deleted_flag, True) + def test_post(self): self.assertFalse( PartnerOrganization.objects.filter(name="New Partner").exists() @@ -484,8 +541,12 @@ def test_post(self): "VENDOR_NAME": "New Partner", "PARTNER_TYPE_DESC": "UN AGENCY", "CSO_TYPE": "National NGO", - "TOTAL_CASH_TRANSFERRED_CP": "2,000", "CORE_VALUE_ASSESSMENT_DT": "01-Jan-01", + 'TOTAL_CASH_TRANSFERRED_CP': "2,000", + 'TOTAL_CASH_TRANSFERRED_CY': "2,000", + 'NET_CASH_TRANSFERRED_CY': "2,000", + 'TOTAL_CASH_TRANSFERRED_YTD': "2,000", + 'REPORTED_CY': "2,000", "COUNTRY": "239", } } diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index cc003991fa..b09678d2ad 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -472,7 +472,8 @@ def create(self, request, *args, **kwargs): country = request.user.profile.country partner_sync = PartnerSynchronizer(business_area_code=country.business_area_code) - partner_resp = partner_sync._filter_records([partner_resp]).pop() + checked_partner = partner_sync._filter_records([partner_resp]) + partner_resp = checked_partner.pop() if checked_partner else None if not partner_resp: return Response({"error": 'Partner skipped because one or more of the required fields are missing'}, status=status.HTTP_400_BAD_REQUEST) From cdcc7d32fdf70873ac855132332aa552ef68d8f2 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Thu, 8 Aug 2019 23:50:50 +0300 Subject: [PATCH 30/56] remove unnecessary code --- .../applications/partners/views/partner_organization_v2.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index b09678d2ad..0d63716d8b 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -472,9 +472,7 @@ def create(self, request, *args, **kwargs): country = request.user.profile.country partner_sync = PartnerSynchronizer(business_area_code=country.business_area_code) - checked_partner = partner_sync._filter_records([partner_resp]) - partner_resp = checked_partner.pop() if checked_partner else None - if not partner_resp: + if not partner_sync._filter_records([partner_resp]): return Response({"error": 'Partner skipped because one or more of the required fields are missing'}, status=status.HTTP_400_BAD_REQUEST) From 83ecae2bf5c927592ea1ccec3daac5ea94f34195 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 6 Aug 2019 13:02:38 -0400 Subject: [PATCH 31/56] 13413 PRP API: Intervention added type --- .../partners/serializers/prp_v1.py | 2 +- .../tests/data/prp-intervention-list.json | 1 + .../applications/partners/views/prp_v1.py | 88 +++++++------------ 3 files changed, 34 insertions(+), 57 deletions(-) diff --git a/src/etools/applications/partners/serializers/prp_v1.py b/src/etools/applications/partners/serializers/prp_v1.py index 6fb6b2ea89..9d919e686f 100644 --- a/src/etools/applications/partners/serializers/prp_v1.py +++ b/src/etools/applications/partners/serializers/prp_v1.py @@ -222,7 +222,6 @@ class Meta: class PRPInterventionListSerializer(serializers.ModelSerializer): - # todo: do these need to be lowercased? amendments = InterventionAmendmentSerializer(read_only=True, many=True) offices = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name') business_area_code = serializers.SerializerMethodField() @@ -280,6 +279,7 @@ class Meta: fields = ( 'id', 'title', + 'document_type', 'business_area_code', 'offices', 'number', diff --git a/src/etools/applications/partners/tests/data/prp-intervention-list.json b/src/etools/applications/partners/tests/data/prp-intervention-list.json index 32cfecc0d3..b03abc1f87 100644 --- a/src/etools/applications/partners/tests/data/prp-intervention-list.json +++ b/src/etools/applications/partners/tests/data/prp-intervention-list.json @@ -88,6 +88,7 @@ "cso_budget_currency": "", "id": 642, "cso_budget": "0.00", + "document_type": "PD", "update_date": "2017-10-12T08:10:43.708677Z" } ] diff --git a/src/etools/applications/partners/views/prp_v1.py b/src/etools/applications/partners/views/prp_v1.py index d498783589..91dba3e72a 100644 --- a/src/etools/applications/partners/views/prp_v1.py +++ b/src/etools/applications/partners/views/prp_v1.py @@ -1,12 +1,8 @@ -import functools -import operator - -from django.db.models import Q - from rest_framework.generics import get_object_or_404, ListAPIView from rest_framework.pagination import LimitOffsetPagination from rest_framework.response import Response from rest_framework.views import APIView +from unicef_restlib.views import QueryStringFilterMixin from etools.applications.partners.filters import PartnerScopeFilter from etools.applications.partners.models import Intervention, PartnerOrganization @@ -40,7 +36,7 @@ class PRPPartnerPagination(LimitOffsetPagination): default_limit = 300 -class PRPInterventionListAPIView(ListAPIView): +class PRPInterventionListAPIView(QueryStringFilterMixin, ListAPIView): """ Create new Interventions. Returns a list of Interventions. @@ -49,52 +45,37 @@ class PRPInterventionListAPIView(ListAPIView): permission_classes = (ListCreateAPIMixedPermission, ) filter_backends = (PartnerScopeFilter,) pagination_class = PRPInterventionPagination - - def paginate_queryset(self, queryset): - return super().paginate_queryset(queryset) + queryset = Intervention.objects.prefetch_related( + 'result_links__cp_output', + 'result_links__ll_results', + 'result_links__ll_results__applied_indicators__indicator', + 'result_links__ll_results__applied_indicators__disaggregation__disaggregation_values', + 'result_links__ll_results__applied_indicators__locations', + 'special_reporting_requirements', + 'reporting_requirements', + 'frs', + 'partner_focal_points', + 'unicef_focal_points', + 'agreement__authorized_officers', + 'agreement__partner', + 'amendments', + 'flat_locations', + 'sections' + ).exclude(status=Intervention.DRAFT) + + filters = ( + ('country_programme', 'agreement__country_programme'), + ('section', 'sections__pk'), + ('status', 'status'), + ('partner', 'agreement__partner'), + ('updated_before', 'modified__lte'), + ('updated_after', 'modified__gte'), + ) def get_queryset(self, format=None): - q = Intervention.objects.prefetch_related( - 'result_links__cp_output', - 'result_links__ll_results', - 'result_links__ll_results__applied_indicators__indicator', - 'result_links__ll_results__applied_indicators__disaggregation__disaggregation_values', - 'result_links__ll_results__applied_indicators__locations', - 'special_reporting_requirements', - 'reporting_requirements', - 'frs', - 'partner_focal_points', - 'unicef_focal_points', - 'agreement__authorized_officers', - 'agreement__partner', - 'amendments', - 'flat_locations', - 'sections' - ).exclude(status=Intervention.DRAFT) - - query_params = self.request.query_params - workspace = query_params.get('workspace', None) + workspace = self.request.query_params.get('workspace', None) set_tenant_or_fail(workspace) - - if query_params: - queries = [] - if "country_programme" in query_params.keys(): - queries.append(Q(agreement__country_programme=query_params.get("country_programme"))) - if "section" in query_params.keys(): - queries.append(Q(sections__pk=query_params.get("section"))) - if "status" in query_params.keys(): - queries.append(Q(status=query_params.get("status"))) - if "partner" in query_params.keys(): - queries.append(Q(agreement__partner=query_params.get("partner"))) - if "updated_before" in query_params.keys(): - queries.append(Q(modified__lte=query_params.get("updated_before"))) - if "updated_after" in query_params.keys(): - queries.append(Q(modified__gte=query_params.get("updated_after"))) - if queries: - expression = functools.reduce(operator.and_, queries) - q = q.filter(expression).distinct() - - return q + return super().get_queryset() class PRPPartnerListAPIView(ListAPIView): @@ -105,15 +86,10 @@ class PRPPartnerListAPIView(ListAPIView): serializer_class = PRPPartnerOrganizationListSerializer permission_classes = (ListCreateAPIMixedPermission, ) pagination_class = PRPPartnerPagination - - def paginate_queryset(self, queryset): - return super().paginate_queryset(queryset) + queryset = PartnerOrganization.objects.all() def get_queryset(self, format=None): - q = PartnerOrganization.objects.all() - query_params = self.request.query_params workspace = query_params.get('workspace', None) set_tenant_or_fail(workspace) - - return q + return super().get_queryset() From f2d91ae09ea0f636574080490a2cfcfac4782b48 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 8 Aug 2019 16:48:33 -0400 Subject: [PATCH 32/56] 13207 added disbursement --- .../partners/serializers/prp_v1.py | 23 ++++++++++++++++++- .../tests/data/prp-intervention-list.json | 2 ++ .../applications/partners/views/prp_v1.py | 9 +++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/serializers/prp_v1.py b/src/etools/applications/partners/serializers/prp_v1.py index 9d919e686f..e27e6b7bce 100644 --- a/src/etools/applications/partners/serializers/prp_v1.py +++ b/src/etools/applications/partners/serializers/prp_v1.py @@ -252,6 +252,25 @@ class PRPInterventionListSerializer(serializers.ModelSerializer): sections = SectionSerializer(many=True, read_only=True) locations = PRPLocationSerializer(source="flat_locations", many=True, read_only=True) + disbursement = serializers.DecimalField(source='frs__actual_amt_local__sum', read_only=True, + max_digits=20, + decimal_places=2) + + disbursement_percent = serializers.SerializerMethodField() + + def fr_currencies_ok(self, obj): + return obj.frs__currency__count == 1 if obj.frs__currency__count else None + + def get_disbursement_percent(self, obj): + if obj.frs__actual_amt_local__sum is None: + return None + + if not (self.fr_currencies_ok(obj) and obj.max_fr_currency == obj.planned_budget.currency): + return "%.1f" % -1.0 + percent = obj.frs__actual_amt_local__sum / obj.total_unicef_cash * 100 \ + if obj.total_unicef_cash and obj.total_unicef_cash > 0 else 0 + return "%.1f" % percent + def get_unicef_budget_currency(self, obj): # Intervention.planned_budget isn't a real field, it's a related # name from an InterventionBudget, and there might not be one. @@ -300,5 +319,7 @@ class Meta: 'amendments', 'locations', 'unicef_budget_cash', - 'unicef_budget_supplies' + 'unicef_budget_supplies', + 'disbursement', + 'disbursement_percent' ) diff --git a/src/etools/applications/partners/tests/data/prp-intervention-list.json b/src/etools/applications/partners/tests/data/prp-intervention-list.json index b03abc1f87..ea7dc5d55c 100644 --- a/src/etools/applications/partners/tests/data/prp-intervention-list.json +++ b/src/etools/applications/partners/tests/data/prp-intervention-list.json @@ -88,6 +88,8 @@ "cso_budget_currency": "", "id": 642, "cso_budget": "0.00", + "disbursement": null, + "disbursement_percent": null, "document_type": "PD", "update_date": "2017-10-12T08:10:43.708677Z" } diff --git a/src/etools/applications/partners/views/prp_v1.py b/src/etools/applications/partners/views/prp_v1.py index 91dba3e72a..c65ef02dec 100644 --- a/src/etools/applications/partners/views/prp_v1.py +++ b/src/etools/applications/partners/views/prp_v1.py @@ -1,3 +1,5 @@ +from django.db.models import CharField, Count, Sum + from rest_framework.generics import get_object_or_404, ListAPIView from rest_framework.pagination import LimitOffsetPagination from rest_framework.response import Response @@ -13,6 +15,7 @@ PRPPartnerOrganizationListSerializer, ) from etools.applications.partners.views.helpers import set_tenant_or_fail +from etools.libraries.djangolib.models import MaxDistinct class PRPPDFileView(APIView): @@ -61,7 +64,11 @@ class PRPInterventionListAPIView(QueryStringFilterMixin, ListAPIView): 'amendments', 'flat_locations', 'sections' - ).exclude(status=Intervention.DRAFT) + ).exclude(status=Intervention.DRAFT).annotate( + Count("frs__currency", distinct=True), + Sum("frs__actual_amt_local"), + max_fr_currency=MaxDistinct("frs__currency", output_field=CharField(), distinct=True), + ) filters = ( ('country_programme', 'agreement__country_programme'), From 38470451bca9205b9f0309a533a0fe971bc8ee08 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 19 Jul 2019 14:10:44 -0400 Subject: [PATCH 33/56] Add attachments to engagement serializers --- .../audit/serializers/engagement.py | 19 ++++++++++---- .../applications/audit/tests/test_views.py | 25 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 1d5c4fa75c..099ffe4788 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -3,9 +3,9 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from unicef_attachments.fields import FileTypeModelChoiceField +from unicef_attachments.fields import AttachmentSingleFileField, FileTypeModelChoiceField from unicef_attachments.models import FileType -from unicef_attachments.serializers import BaseAttachmentSerializer +from unicef_attachments.serializers import AttachmentSerializerMixin, BaseAttachmentSerializer from unicef_restlib.fields import SeparatedReadWriteField from unicef_restlib.serializers import WritableNestedParentSerializerMixin, WritableNestedSerializerMixin @@ -220,9 +220,12 @@ class Meta(WritableNestedSerializerMixin.Meta): ] -class EngagementSerializer(EngagementDatesValidation, - WritableNestedParentSerializerMixin, - EngagementListSerializer): +class EngagementSerializer( + AttachmentSerializerMixin, + EngagementDatesValidation, + WritableNestedParentSerializerMixin, + EngagementListSerializer +): staff_members = SeparatedReadWriteField( read_field=AuditorStaffMemberSerializer(many=True, required=False), label=_('Audit Staff Team Members') ) @@ -235,6 +238,9 @@ class EngagementSerializer(EngagementDatesValidation, ) specific_procedures = SpecificProcedureSerializer(many=True, label=_('Specific Procedure To Be Performed')) + engagement_attachments = AttachmentSingleFileField(required=False) + report_attachments = AttachmentSingleFileField(required=False) + final_report = AttachmentSingleFileField(required=False) class Meta(EngagementListSerializer.Meta): fields = EngagementListSerializer.Meta.fields + [ @@ -249,6 +255,9 @@ class Meta(EngagementListSerializer.Meta): 'date_of_draft_report_to_unicef', 'date_of_comments_by_unicef', 'date_of_report_submit', 'date_of_final_report', 'date_of_cancel', 'cancel_comment', 'specific_procedures', + 'engagement_attachments', + 'report_attachments', + 'final_report', ] extra_kwargs = { field: {'required': True} for field in [ diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 7dd6e8454b..465fd425c5 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -1,4 +1,5 @@ import datetime +import json import random from django.core.files.uploadedfile import SimpleUploadedFile @@ -488,6 +489,30 @@ def test_government_partner_without_active_pd(self): self.assertEquals(response.status_code, status.HTTP_201_CREATED) + def test_attachments(self): + file_type_engagement = AttachmentFileTypeFactory( + code="audit_engagement", + ) + attachment_engagement = AttachmentFactory( + file="test_engagement.pdf", + file_type=None, + code="", + ) + self.create_data["engagement_attachments"] = attachment_engagement.pk + + response = self._do_create(self.unicef_focal_point, self.create_data) + + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + + data = json.loads(response.content) + engagement_val = data["engagement_attachments"] + self.assertIsNotNone(engagement_val) + self.assertTrue( + engagement_val.endswith(attachment_engagement.file.name) + ) + attachment_engagement.refresh_from_db() + self.assertEqual(attachment_engagement.file_type, file_type_engagement) + class TestMicroAssessmentCreateViewSet(TestEngagementCreateActivePDViewSet, BaseTestEngagementsCreateViewSet, BaseTenantTestCase): From 19fd66d52d33f3c9a7fdf862e3a35c09f3f8751d Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 5 Aug 2019 09:53:48 -0400 Subject: [PATCH 34/56] Update audit EngagementAttachmentsViewSet handle get, post, and patch --- .../audit/serializers/engagement.py | 23 +++++-- .../applications/audit/tests/test_views.py | 64 ++++++++++++++++--- src/etools/applications/audit/views.py | 13 ++++ 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 099ffe4788..4f960381f1 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -4,7 +4,7 @@ from rest_framework import serializers from unicef_attachments.fields import AttachmentSingleFileField, FileTypeModelChoiceField -from unicef_attachments.models import FileType +from unicef_attachments.models import FileType, Attachment from unicef_attachments.serializers import AttachmentSerializerMixin, BaseAttachmentSerializer from unicef_restlib.fields import SeparatedReadWriteField from unicef_restlib.serializers import WritableNestedParentSerializerMixin, WritableNestedSerializerMixin @@ -65,17 +65,26 @@ class Meta(PartnerOrganizationListSerializer.Meta): } -class EngagementAttachmentSerializer(BaseAttachmentSerializer): +class EngagementAttachmentSerializer(serializers.ModelSerializer): + pk = serializers.IntegerField() file_type = FileTypeModelChoiceField( - label=_('Document Type'), queryset=FileType.objects.filter(code='audit_engagement') + label=_('Document Type'), + queryset=FileType.objects.filter(code='audit_engagement'), ) + url = serializers.SerializerMethodField() - class Meta(BaseAttachmentSerializer.Meta): - pass + class Meta: + model = Attachment + fields = ("pk", "file_type", "url", "uploaded_by") - def create(self, validated_data): + def update(self, instance, validated_data): validated_data['code'] = 'audit_engagement' - return super().create(validated_data) + return super().update(instance, validated_data) + + def get_url(self, obj): + if obj.file: + return obj.file.url + return "" class ReportAttachmentSerializer(BaseAttachmentSerializer): diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 465fd425c5..b62cb95d83 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -2,6 +2,7 @@ import json import random +from django.contrib.contenttypes.models import ContentType from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.urls import reverse @@ -1209,28 +1210,71 @@ def test_get(self): class TestEngagementAttachmentsView(MATransitionsTestCaseMixin, BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.file_type = AttachmentFileTypeFactory(code='audit_engagement') + def test_list(self): attachments_num = self.engagement.engagement_attachments.count() + response = self.forced_auth_req( + 'get', + reverse( + 'audit:engagement-attachments-list', + args=[self.engagement.pk], + ), + user=self.unicef_focal_point + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), attachments_num) - create_response = self.forced_auth_req( + def test_post(self): + attachment = AttachmentFactory(file="sample.pdf") + self.assertIsNone(attachment.object_id) + self.assertNotEqual(attachment.code, "audit_engagement") + + response = self.forced_auth_req( 'post', - reverse('audit:engagement-attachments-list', args=[self.engagement.id]), + reverse( + 'audit:engagement-attachments-list', + args=[self.engagement.pk], + ), user=self.unicef_focal_point, - request_format='multipart', data={ - 'file_type': AttachmentFileTypeFactory(code='audit_engagement').id, - 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), + 'file_type': self.file_type.pk, + 'pk': attachment.pk, } ) - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + attachment.refresh_from_db() + self.assertEqual(attachment.object_id, self.engagement.pk) + self.assertEqual(attachment.code, "audit_engagement") + + def test_patch(self): + file_type_old = AttachmentFileTypeFactory(code="different_engagement") + attachment = AttachmentFactory( + file="sample.pdf", + file_type=file_type_old, + content_type=ContentType.objects.get_for_model(Engagement), + object_id=self.engagement.pk, + code="audit_engagement", + ) + self.assertNotEqual(attachment.file_type, self.file_type) response = self.forced_auth_req( - 'get', - reverse('audit:engagement-attachments-list', args=[self.engagement.id]), - user=self.unicef_focal_point + 'patch', + reverse( + 'audit:engagement-attachments-detail', + args=[self.engagement.pk, attachment.pk], + ), + user=self.unicef_focal_point, + data={ + "file_type": self.file_type.pk, + } ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), attachments_num + 1) + attachment.refresh_from_db() + self.assertEqual(attachment.file_type, self.file_type) def test_create_meta_focal_point(self): response = self.forced_auth_req( diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index b81a37a7c8..6e184b7ff7 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -591,7 +591,20 @@ def get_parent_filter(self): 'object_id': parent.pk } + def get_object(self, pk=None): + if pk: + return self.queryset.get(pk=pk) + elif self.kwargs.get("pk"): + return self.queryset.get(pk=self.kwargs.get("pk")) + return super().get_object() + def perform_create(self, serializer): + serializer.instance = self.get_object( + pk=serializer.validated_data.get("pk") + ) + serializer.save(content_object=self.get_parent_object().get_subclass()) + + def perform_update(self, serializer): serializer.save(content_object=self.get_parent_object().get_subclass()) From d83ec8dd06621264eba861b19aa3af65eea92d0d Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 5 Aug 2019 09:59:17 -0400 Subject: [PATCH 35/56] Update audit EngagementReportAttachmentsViewSet handle get, post, and patch --- .../audit/serializers/engagement.py | 21 ++++-- .../applications/audit/tests/test_views.py | 66 +++++++++++++++---- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 4f960381f1..6c68bb7475 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -87,17 +87,26 @@ def get_url(self, obj): return "" -class ReportAttachmentSerializer(BaseAttachmentSerializer): +class ReportAttachmentSerializer(serializers.ModelSerializer): + pk = serializers.IntegerField() file_type = FileTypeModelChoiceField( - label=_('Document Type'), queryset=FileType.objects.filter(code='audit_report') + label=_('Document Type'), + queryset=FileType.objects.filter(code='audit_report'), ) + url = serializers.SerializerMethodField() - class Meta(BaseAttachmentSerializer.Meta): - pass + class Meta: + model = Attachment + fields = ("pk", "file_type", "url", "uploaded_by") - def create(self, validated_data): + def update(self, instance, validated_data): validated_data['code'] = 'audit_report' - return super().create(validated_data) + return super().update(instance, validated_data) + + def get_url(self, obj): + if obj.file: + return obj.file.url + return "" class EngagementActionPointSerializer(PermissionsBasedSerializerMixin, ActionPointBaseSerializer): diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index b62cb95d83..87a54bdadd 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -1298,25 +1298,69 @@ def test_create_meta_unicef_user(self): class TestEngagementReportAttachmentsView(MATransitionsTestCaseMixin, BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.file_type = AttachmentFileTypeFactory(code='audit_report') + def test_list(self): attachments_num = self.engagement.report_attachments.count() - create_response = self.forced_auth_req( + response = self.forced_auth_req( + 'get', + reverse( + 'audit:report-attachments-list', + args=[self.engagement.pk], + ), + user=self.auditor + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), attachments_num) + + def test_post(self): + attachment = AttachmentFactory(file="sample.pdf") + self.assertIsNone(attachment.object_id) + self.assertNotEqual(attachment.code, "audit_report") + + response = self.forced_auth_req( 'post', - reverse('audit:report-attachments-list', args=[self.engagement.id]), - user=self.auditor, - request_format='multipart', + reverse( + 'audit:report-attachments-list', + args=[self.engagement.pk], + ), + user=self.unicef_focal_point, data={ - 'file_type': AttachmentFileTypeFactory(code='audit_report').id, - 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), + 'file_type': self.file_type.pk, + 'pk': attachment.pk, } ) - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + attachment.refresh_from_db() + self.assertEqual(attachment.object_id, self.engagement.pk) + self.assertEqual(attachment.code, "audit_report") + + def test_patch(self): + file_type_old = AttachmentFileTypeFactory(code="different_report") + attachment = AttachmentFactory( + file="sample.pdf", + file_type=file_type_old, + content_type=ContentType.objects.get_for_model(Engagement), + object_id=self.engagement.pk, + code="audit_report", + ) + self.assertNotEqual(attachment.file_type, self.file_type) response = self.forced_auth_req( - 'get', - reverse('audit:report-attachments-list', args=[self.engagement.id]), - user=self.auditor + 'patch', + reverse( + 'audit:report-attachments-detail', + args=[self.engagement.pk, attachment.pk], + ), + user=self.unicef_focal_point, + data={ + "file_type": self.file_type.pk, + } ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), attachments_num + 1) + attachment.refresh_from_db() + self.assertEqual(attachment.file_type, self.file_type) From 5b0ff2be2763a700a48e2f934555bcffd0bf9102 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 5 Aug 2019 10:00:28 -0400 Subject: [PATCH 36/56] flake8 cleanup --- src/etools/applications/audit/serializers/engagement.py | 2 +- src/etools/applications/audit/tests/test_views.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 6c68bb7475..0444dfe828 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -5,7 +5,7 @@ from rest_framework import serializers from unicef_attachments.fields import AttachmentSingleFileField, FileTypeModelChoiceField from unicef_attachments.models import FileType, Attachment -from unicef_attachments.serializers import AttachmentSerializerMixin, BaseAttachmentSerializer +from unicef_attachments.serializers import AttachmentSerializerMixin from unicef_restlib.fields import SeparatedReadWriteField from unicef_restlib.serializers import WritableNestedParentSerializerMixin, WritableNestedSerializerMixin diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 87a54bdadd..57d6f80ade 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -3,7 +3,6 @@ import random from django.contrib.contenttypes.models import ContentType -from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.urls import reverse From d25929d47e28a5229e98e2471628846ef88322c3 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 5 Aug 2019 10:14:48 -0400 Subject: [PATCH 37/56] flake8 cleanup --- src/etools/applications/audit/serializers/engagement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 0444dfe828..02a34dac48 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -4,7 +4,7 @@ from rest_framework import serializers from unicef_attachments.fields import AttachmentSingleFileField, FileTypeModelChoiceField -from unicef_attachments.models import FileType, Attachment +from unicef_attachments.models import Attachment, FileType from unicef_attachments.serializers import AttachmentSerializerMixin from unicef_restlib.fields import SeparatedReadWriteField from unicef_restlib.serializers import WritableNestedParentSerializerMixin, WritableNestedSerializerMixin From 0651036f15cf4456746d5ff368ec269a34b620bc Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 8 Aug 2019 07:49:41 -0400 Subject: [PATCH 38/56] Update engagement and report attachments param to attachment --- .../audit/serializers/engagement.py | 39 +++++++++++-------- .../applications/audit/tests/test_views.py | 4 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 02a34dac48..65646b869e 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -65,49 +65,56 @@ class Meta(PartnerOrganizationListSerializer.Meta): } +class AttachmentField(serializers.Field): + def to_representation(self, value): + if not value: + return None + + attachment = Attachment.objects.get(pk=value) + if not getattr(attachment.file, "url", None): + return None + + url = attachment.file.url + request = self.context.get('request', None) + if request is not None: + return request.build_absolute_uri(url) + return url + + def to_internal_value(self, data): + return data + + class EngagementAttachmentSerializer(serializers.ModelSerializer): - pk = serializers.IntegerField() + attachment = AttachmentField(source="pk") file_type = FileTypeModelChoiceField( label=_('Document Type'), queryset=FileType.objects.filter(code='audit_engagement'), ) - url = serializers.SerializerMethodField() class Meta: model = Attachment - fields = ("pk", "file_type", "url", "uploaded_by") + fields = ("attachment", "file_type") def update(self, instance, validated_data): validated_data['code'] = 'audit_engagement' return super().update(instance, validated_data) - def get_url(self, obj): - if obj.file: - return obj.file.url - return "" - class ReportAttachmentSerializer(serializers.ModelSerializer): - pk = serializers.IntegerField() + attachment = AttachmentField(source="pk") file_type = FileTypeModelChoiceField( label=_('Document Type'), queryset=FileType.objects.filter(code='audit_report'), ) - url = serializers.SerializerMethodField() class Meta: model = Attachment - fields = ("pk", "file_type", "url", "uploaded_by") + fields = ("attachment", "file_type") def update(self, instance, validated_data): validated_data['code'] = 'audit_report' return super().update(instance, validated_data) - def get_url(self, obj): - if obj.file: - return obj.file.url - return "" - class EngagementActionPointSerializer(PermissionsBasedSerializerMixin, ActionPointBaseSerializer): reference_number = serializers.ReadOnlyField(label=_('Reference No.')) diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 57d6f80ade..503c4beafa 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -1241,7 +1241,7 @@ def test_post(self): user=self.unicef_focal_point, data={ 'file_type': self.file_type.pk, - 'pk': attachment.pk, + 'attachment': attachment.pk, } ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -1330,7 +1330,7 @@ def test_post(self): user=self.unicef_focal_point, data={ 'file_type': self.file_type.pk, - 'pk': attachment.pk, + 'attachment': attachment.pk, } ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) From b02cb8e7a4a385f2dd7067264d49d45b40c2c5d1 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 13 Aug 2019 08:55:00 -0400 Subject: [PATCH 39/56] Add id, and created to audit engagement attachments --- .../audit/serializers/engagement.py | 6 +- .../applications/audit/tests/test_views.py | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 65646b869e..0813051aa4 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -85,6 +85,7 @@ def to_internal_value(self, data): class EngagementAttachmentSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(read_only=True) attachment = AttachmentField(source="pk") file_type = FileTypeModelChoiceField( label=_('Document Type'), @@ -93,7 +94,7 @@ class EngagementAttachmentSerializer(serializers.ModelSerializer): class Meta: model = Attachment - fields = ("attachment", "file_type") + fields = ("id", "attachment", "file_type", "created") def update(self, instance, validated_data): validated_data['code'] = 'audit_engagement' @@ -101,6 +102,7 @@ def update(self, instance, validated_data): class ReportAttachmentSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(read_only=True) attachment = AttachmentField(source="pk") file_type = FileTypeModelChoiceField( label=_('Document Type'), @@ -109,7 +111,7 @@ class ReportAttachmentSerializer(serializers.ModelSerializer): class Meta: model = Attachment - fields = ("attachment", "file_type") + fields = ("id", "attachment", "file_type", "created") def update(self, instance, validated_data): validated_data['code'] = 'audit_report' diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 503c4beafa..1d4c0bc773 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -9,6 +9,7 @@ from factory import fuzzy from mock import Mock, patch from rest_framework import status +from unicef_attachments.models import Attachment from etools.applications.action_points.tests.factories import ActionPointCategoryFactory, ActionPointFactory from etools.applications.attachments.tests.factories import AttachmentFactory, AttachmentFileTypeFactory @@ -1215,6 +1216,14 @@ def setUpTestData(cls): cls.file_type = AttachmentFileTypeFactory(code='audit_engagement') def test_list(self): + attachment = AttachmentFactory( + file="sample.pdf", + file_type=self.file_type, + content_type=ContentType.objects.get_for_model(Engagement), + object_id=self.engagement.pk, + code="audit_engagement", + ) + self.engagement.engagement_attachments.add(attachment) attachments_num = self.engagement.engagement_attachments.count() response = self.forced_auth_req( 'get', @@ -1275,6 +1284,30 @@ def test_patch(self): attachment.refresh_from_db() self.assertEqual(attachment.file_type, self.file_type) + def test_delete(self): + attachment = AttachmentFactory( + file="sample.pdf", + file_type=self.file_type, + content_type=ContentType.objects.get_for_model(Engagement), + object_id=self.engagement.pk, + code="audit_engagement", + ) + self.engagement.engagement_attachments.add(attachment) + attachment_qs = self.engagement.engagement_attachments + attachments_num = attachment_qs.count() + + response = self.forced_auth_req( + 'delete', + reverse( + 'audit:engagement-attachments-detail', + args=[self.engagement.pk, attachment.pk], + ), + user=self.unicef_focal_point, + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(attachment_qs.count(), attachments_num - 1) + self.assertFalse(Attachment.objects.filter(pk=attachment.pk).exists()) + def test_create_meta_focal_point(self): response = self.forced_auth_req( 'options', @@ -1303,6 +1336,14 @@ def setUpTestData(cls): cls.file_type = AttachmentFileTypeFactory(code='audit_report') def test_list(self): + attachment = AttachmentFactory( + file="sample.pdf", + file_type=self.file_type, + content_type=ContentType.objects.get_for_model(Engagement), + object_id=self.engagement.pk, + code="audit_report", + ) + self.engagement.report_attachments.add(attachment) attachments_num = self.engagement.report_attachments.count() response = self.forced_auth_req( @@ -1363,3 +1404,27 @@ def test_patch(self): self.assertEqual(response.status_code, status.HTTP_200_OK) attachment.refresh_from_db() self.assertEqual(attachment.file_type, self.file_type) + + def test_delete(self): + attachment = AttachmentFactory( + file="sample.pdf", + file_type=self.file_type, + content_type=ContentType.objects.get_for_model(Engagement), + object_id=self.engagement.pk, + code="audit_report", + ) + self.engagement.engagement_attachments.add(attachment) + attachment_qs = self.engagement.report_attachments + attachments_num = attachment_qs.count() + + response = self.forced_auth_req( + 'delete', + reverse( + 'audit:report-attachments-detail', + args=[self.engagement.pk, attachment.pk], + ), + user=self.unicef_focal_point, + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(attachment_qs.count(), attachments_num - 1) + self.assertFalse(Attachment.objects.filter(pk=attachment.pk).exists()) From 5b1b6b2ef412a1fe9e04917066217acdea6c485d Mon Sep 17 00:00:00 2001 From: robertavram Date: Fri, 16 Aug 2019 11:50:59 -0400 Subject: [PATCH 40/56] Lower indicator tweaks Fixes [ch13199] [ch13201] --- src/etools/applications/reports/models.py | 3 ++- src/etools/applications/reports/serializers/v2.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/reports/models.py b/src/etools/applications/reports/models.py index e3e15c5941..c330e5e06c 100644 --- a/src/etools/applications/reports/models.py +++ b/src/etools/applications/reports/models.py @@ -370,7 +370,7 @@ def __str__(self): class Meta: unique_together = (('result_link', 'code'),) - ordering = ('-created',) + ordering = ('created',) def save(self, **kwargs): if not self.code: @@ -675,6 +675,7 @@ def baseline_display(self): class Meta: unique_together = (("indicator", "lower_result"),) + ordering = ("created",) def save(self, *args, **kwargs): super().save(*args, **kwargs) diff --git a/src/etools/applications/reports/serializers/v2.py b/src/etools/applications/reports/serializers/v2.py index 410294270f..d25ba07f58 100644 --- a/src/etools/applications/reports/serializers/v2.py +++ b/src/etools/applications/reports/serializers/v2.py @@ -59,7 +59,6 @@ class Meta: def create(self, validated_data): # always try to get first - validated_data['title'] = validated_data['title'].title() return IndicatorBlueprint.objects.get_or_create(**validated_data)[0] From 64105af78d19b1f0ef1fe391cf60b5edf4d55dc2 Mon Sep 17 00:00:00 2001 From: robertavram Date: Fri, 16 Aug 2019 12:09:56 -0400 Subject: [PATCH 41/56] Add migration --- .../migrations/0019_auto_20190816_1609.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/etools/applications/reports/migrations/0019_auto_20190816_1609.py diff --git a/src/etools/applications/reports/migrations/0019_auto_20190816_1609.py b/src/etools/applications/reports/migrations/0019_auto_20190816_1609.py new file mode 100644 index 0000000000..5f12f0a839 --- /dev/null +++ b/src/etools/applications/reports/migrations/0019_auto_20190816_1609.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2019-08-16 16:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0018_section_active'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appliedindicator', + options={'ordering': ('created',)}, + ), + migrations.AlterModelOptions( + name='lowerresult', + options={'ordering': ('created',)}, + ), + ] From abc6836cae791d488dd537ada7c6c297e770cef9 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 16 Aug 2019 16:43:45 -0400 Subject: [PATCH 42/56] fix ClusterListAPI --- src/etools/applications/reports/views/v2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/reports/views/v2.py b/src/etools/applications/reports/views/v2.py index d5fc608f91..4d6ddb39a7 100644 --- a/src/etools/applications/reports/views/v2.py +++ b/src/etools/applications/reports/views/v2.py @@ -426,7 +426,8 @@ class ClusterListAPIView(ListAPIView): CSVRenderer, CSVFlatRenderer, ) - queryset = AppliedIndicator.objects.filter(cluster_name__isnull=False).values('cluster_name').distinct() + queryset = AppliedIndicator.objects.filter(cluster_name__isnull=False).order_by( + 'cluster_name').values('cluster_name').distinct() class SpecialReportingRequirementListCreateView(ListCreateAPIView): From 41d6c6e750a8dd1b1817a07a3060e6216ea7ceef Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 19 Aug 2019 12:00:10 -0400 Subject: [PATCH 43/56] tpm activities API --- src/etools/applications/tpm/serializers/visit.py | 10 ++++++++++ src/etools/applications/tpm/urls.py | 2 ++ src/etools/applications/tpm/views.py | 11 +++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/etools/applications/tpm/serializers/visit.py b/src/etools/applications/tpm/serializers/visit.py index a8e1a0896f..194a1ec7a2 100644 --- a/src/etools/applications/tpm/serializers/visit.py +++ b/src/etools/applications/tpm/serializers/visit.py @@ -103,6 +103,16 @@ def create(self, validated_data): return super().create(validated_data) +class TPMActivityLightSerializer(serializers.ModelSerializer): + + class Meta: + model = TPMActivity + fields = [ + 'id', '__str__', 'date', 'tpm_visit', 'is_pv', 'partner', 'intervention', 'cp_output', 'section', + 'unicef_focal_points', 'locations', 'additional_information', 'offices', + ] + + class TPMActivitySerializer(PermissionsBasedSerializerMixin, WritableNestedSerializerMixin, ActivitySerializer): partner = SeparatedReadWriteField( diff --git a/src/etools/applications/tpm/urls.py b/src/etools/applications/tpm/urls.py index ceab4c16a1..aa3ffb2521 100644 --- a/src/etools/applications/tpm/urls.py +++ b/src/etools/applications/tpm/urls.py @@ -9,6 +9,7 @@ ActivityReportAttachmentsViewSet, PartnerAttachmentsViewSet, TPMActionPointViewSet, + TPMActivityViewSet, TPMPartnerViewSet, TPMStaffMembersViewSet, TPMVisitViewSet, @@ -28,6 +29,7 @@ tpm_visits_api = routers.SimpleRouter() tpm_visits_api.register(r'visits', TPMVisitViewSet, basename='visits') +tpm_visits_api.register(r'activities', TPMActivityViewSet, basename='activities') visit_attachments_api = NestedComplexRouter(tpm_visits_api, r'visits') visit_attachments_api.register('report-attachments', VisitReportAttachmentsViewSet, diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index 2ee169bcdc..8a70a4d317 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -72,6 +72,7 @@ ) from etools.applications.tpm.serializers.visit import ( TPMActionPointSerializer, + TPMActivityLightSerializer, TPMVisitDraftSerializer, TPMVisitLightSerializer, TPMVisitSerializer, @@ -458,6 +459,16 @@ def tpm_visit_letter(self, request, *args, **kwargs): ) +class TPMActivityViewSet(viewsets.ReadOnlyModelViewSet): + queryset = TPMActivity.objects.all() + serializer_class = TPMActivityLightSerializer + + filter_backends = (SearchFilter, OrderingFilter, DjangoFilterBackend) + search_fields = ('tpm_visit__tpm_partner__vendor_number', 'tpm_visit__tpm_partner__name', + 'partner__name', 'partner__vendor_number') + filter_fields = ('tpm_visit', 'section', 'offices', 'tpm_visit__tpm_partner', 'partner') + + class TPMActionPointViewSet(BaseTPMViewSet, mixins.ListModelMixin, mixins.CreateModelMixin, From 2ba9931078508dcad841840ab5ac81bae6a993b0 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 19 Aug 2019 13:48:49 -0400 Subject: [PATCH 44/56] added filter --- src/etools/applications/tpm/filters.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/tpm/filters.py b/src/etools/applications/tpm/filters.py index 13a96b1c38..0454d4d95f 100644 --- a/src/etools/applications/tpm/filters.py +++ b/src/etools/applications/tpm/filters.py @@ -22,6 +22,9 @@ class TPMVisitFilter(filters.FilterSet): tpm_activities__offices__in = filters.BaseInFilter( field_name="tpm_activities__offices", ) + tpm_partner_focal_points__in = filters.BaseInFilter( + field_name="tpm_partner_focal_points", + ) class Meta: model = TPMVisit @@ -36,5 +39,5 @@ class Meta: 'tpm_activities__date': ['exact', 'lte', 'gte', 'gt', 'lt'], 'status': ['exact', 'in'], 'tpm_activities__unicef_focal_points': ['exact'], - 'tpm_partner_focal_points': ['exact'], + 'tpm_partner_focal_points': ['exact', 'in'], } From 5e4bb6a19bd3b4b7c5b477d04366d3090577ca28 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 20 Aug 2019 10:25:04 -0400 Subject: [PATCH 45/56] rename serializer --- src/etools/applications/tpm/serializers/visit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/tpm/serializers/visit.py b/src/etools/applications/tpm/serializers/visit.py index 194a1ec7a2..058fc33619 100644 --- a/src/etools/applications/tpm/serializers/visit.py +++ b/src/etools/applications/tpm/serializers/visit.py @@ -105,10 +105,12 @@ def create(self, validated_data): class TPMActivityLightSerializer(serializers.ModelSerializer): + description = serializers.ReadOnlyField(source='__str__') + class Meta: model = TPMActivity fields = [ - 'id', '__str__', 'date', 'tpm_visit', 'is_pv', 'partner', 'intervention', 'cp_output', 'section', + 'id', 'description', 'date', 'tpm_visit', 'is_pv', 'partner', 'intervention', 'cp_output', 'section', 'unicef_focal_points', 'locations', 'additional_information', 'offices', ] From fe1db96e9aea5bffb5e158d95b8a4276c6db32f3 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 20 Aug 2019 11:32:43 -0400 Subject: [PATCH 46/56] multiselector --- src/etools/applications/tpm/filters.py | 25 ++++++++++++++++++------- src/etools/applications/tpm/views.py | 4 ++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/etools/applications/tpm/filters.py b/src/etools/applications/tpm/filters.py index 0454d4d95f..75cfedf9ff 100644 --- a/src/etools/applications/tpm/filters.py +++ b/src/etools/applications/tpm/filters.py @@ -3,7 +3,7 @@ from django_filters import rest_framework as filters from rest_framework.filters import BaseFilterBackend -from etools.applications.tpm.models import TPMVisit +from etools.applications.tpm.models import TPMActivity, TPMVisit class ReferenceNumberOrderingFilter(BaseFilterBackend): @@ -19,12 +19,8 @@ def filter_queryset(self, request, queryset, view): class TPMVisitFilter(filters.FilterSet): - tpm_activities__offices__in = filters.BaseInFilter( - field_name="tpm_activities__offices", - ) - tpm_partner_focal_points__in = filters.BaseInFilter( - field_name="tpm_partner_focal_points", - ) + tpm_activities__offices__in = filters.BaseInFilter(field_name="tpm_activities__offices") + tpm_partner_focal_points__in = filters.BaseInFilter(field_name="tpm_partner_focal_points") class Meta: model = TPMVisit @@ -41,3 +37,18 @@ class Meta: 'tpm_activities__unicef_focal_points': ['exact'], 'tpm_partner_focal_points': ['exact', 'in'], } + + +class TPMActivityFilter(filters.FilterSet): + offices__in = filters.BaseInFilter(field_name="offices") + + class Meta: + model = TPMActivity + fields = { + 'tpm_visit': ['exact'], + 'section': ['exact', 'in'], + 'offices': ['exact', 'in'], + 'tpm_visit__tpm_partner': ['exact'], + 'partner': ['exact', 'in'], + 'tpm_visit__status': ['exact', 'in'], + } diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index 8a70a4d317..06719628dc 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -53,7 +53,7 @@ TPMPartnerExportSerializer, TPMVisitExportSerializer, ) -from etools.applications.tpm.filters import ReferenceNumberOrderingFilter, TPMVisitFilter +from etools.applications.tpm.filters import ReferenceNumberOrderingFilter, TPMActivityFilter, TPMVisitFilter from etools.applications.tpm.models import PME, ThirdPartyMonitor, TPMActionPoint, TPMActivity, TPMVisit, UNICEFUser from etools.applications.tpm.serializers.attachments import ( ActivityAttachmentsSerializer, @@ -462,11 +462,11 @@ def tpm_visit_letter(self, request, *args, **kwargs): class TPMActivityViewSet(viewsets.ReadOnlyModelViewSet): queryset = TPMActivity.objects.all() serializer_class = TPMActivityLightSerializer + filter_class = TPMActivityFilter filter_backends = (SearchFilter, OrderingFilter, DjangoFilterBackend) search_fields = ('tpm_visit__tpm_partner__vendor_number', 'tpm_visit__tpm_partner__name', 'partner__name', 'partner__vendor_number') - filter_fields = ('tpm_visit', 'section', 'offices', 'tpm_visit__tpm_partner', 'partner') class TPMActionPointViewSet(BaseTPMViewSet, From 85a21d13360e127d613f42894b73124991c2f9bd Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 30 Aug 2019 06:56:15 -0400 Subject: [PATCH 47/56] 10175 fixed tpm permissions --- .../tpm/management/commands/update_tpm_permissions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 24e351f95d..27f6196841 100644 --- a/src/etools/applications/tpm/management/commands/update_tpm_permissions.py +++ b/src/etools/applications/tpm/management/commands/update_tpm_permissions.py @@ -295,9 +295,10 @@ def assign_permissions(self): self.add_permissions(self.unicef_user, 'view', self.action_points_block, condition=tpm_reported_condition) - self.add_permissions(self.pme, 'view', 'tpm.tpmactivity.pv_applicable', + self.add_permissions([self.pme, self.focal_point], 'view', 'tpm.tpmactivity.pv_applicable', condition=tpm_reported_condition) - self.add_permissions(self.pme, 'edit', ['tpm.tpmvisit.approval_comment', 'tpm.tpmvisit.report_reject_comments'], + self.add_permissions([self.pme, self.focal_point], 'edit', + ['tpm.tpmvisit.approval_comment', 'tpm.tpmvisit.report_reject_comments'], condition=tpm_reported_condition) self.add_permissions([self.pme, self.focal_point], 'action', ['tpm.tpmvisit.approve', 'tpm.tpmvisit.reject_report'], From 1e346cc008f3cede3dc7650fb9bccc10821357a8 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Mon, 26 Aug 2019 22:19:41 +0300 Subject: [PATCH 48/56] trying to readd Unicef logo to the TPMvisit PDF letter --- .../tpm/templates/tpm/visit_letter_pdf.html | 4 ++-- src/etools/assets/images/BFidPn.png | Bin 0 -> 61338 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/etools/assets/images/BFidPn.png diff --git a/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html b/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html index cd26de7e11..71ab043695 100644 --- a/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html +++ b/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html @@ -57,7 +57,7 @@ {% block page_header %} @@ -93,7 +93,7 @@

UNICEF kindly asks the reader of this letter to facilitate the work of the staff of {{ visit.tpm_partner.name }} in order to carry out these visits.

- + diff --git a/src/etools/assets/images/BFidPn.png b/src/etools/assets/images/BFidPn.png new file mode 100644 index 0000000000000000000000000000000000000000..f97f338e9600a091f6a331d5eb17c7bd378e3f69 GIT binary patch literal 61338 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 literal 0 HcmV?d00001 From ad75abe9baca089bf372c6c1368c60a404678941 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Tue, 27 Aug 2019 17:02:29 +0300 Subject: [PATCH 49/56] rename logo images --- .../tpm/templates/tpm/visit_letter_pdf.html | 4 ++-- .../images/{BFidPn.png => letter_logo_bottom.png} | Bin .../images/{qDWQF3.png => letter_logo_top.png} | Bin 3 files changed, 2 insertions(+), 2 deletions(-) rename src/etools/assets/images/{BFidPn.png => letter_logo_bottom.png} (100%) rename src/etools/assets/images/{qDWQF3.png => letter_logo_top.png} (100%) diff --git a/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html b/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html index 71ab043695..222fcb4f2e 100644 --- a/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html +++ b/src/etools/applications/tpm/templates/tpm/visit_letter_pdf.html @@ -57,7 +57,7 @@ {% block page_header %} @@ -93,7 +93,7 @@

UNICEF kindly asks the reader of this letter to facilitate the work of the staff of {{ visit.tpm_partner.name }} in order to carry out these visits.

- + diff --git a/src/etools/assets/images/BFidPn.png b/src/etools/assets/images/letter_logo_bottom.png similarity index 100% rename from src/etools/assets/images/BFidPn.png rename to src/etools/assets/images/letter_logo_bottom.png diff --git a/src/etools/assets/images/qDWQF3.png b/src/etools/assets/images/letter_logo_top.png similarity index 100% rename from src/etools/assets/images/qDWQF3.png rename to src/etools/assets/images/letter_logo_top.png From cc00f178837f8a97b96947dcb6ca02fc85bf6a99 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 28 Aug 2019 16:44:29 -0400 Subject: [PATCH 50/56] 13084 update tpm permissions --- src/etools/applications/tpm/admin.py | 1 + .../tpm/management/commands/update_tpm_permissions.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/etools/applications/tpm/admin.py b/src/etools/applications/tpm/admin.py index a8b391d843..5968088edb 100644 --- a/src/etools/applications/tpm/admin.py +++ b/src/etools/applications/tpm/admin.py @@ -8,6 +8,7 @@ class TPMActivityAdmin(admin.ModelAdmin): list_display = ( '__str__', + 'is_pv' ) search_fields = ( 'tpm_visit__author__username', 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 27f6196841..fb51d12c30 100644 --- a/src/etools/applications/tpm/management/commands/update_tpm_permissions.py +++ b/src/etools/applications/tpm/management/commands/update_tpm_permissions.py @@ -243,8 +243,13 @@ def assign_permissions(self): condition=self.visit_status(TPMVisit.STATUSES.draft)) # visit cancelled + tpm_cancelled_condition = self.visit_status(TPMVisit.STATUSES.cancelled) self.add_permissions([self.unicef_user, self.third_party_monitor], 'view', ['tpm.tpmvisit.cancel_comment'], condition=self.visit_status(TPMVisit.STATUSES.cancelled)) + self.add_permissions(self.third_party_monitor, 'view', self.tpm_visit_details, + condition=tpm_cancelled_condition) + self.add_permissions(self.third_party_monitor, 'view', self.visit_report, + condition=tpm_cancelled_condition) # visit assigned self.add_permissions(self.third_party_monitor, 'view', self.tpm_visit_details, From 45c3d56334d94653512cf8e9d04bbeaa44182914 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 21 Aug 2019 17:07:28 -0400 Subject: [PATCH 51/56] update small things --- .gitignore | 5 ----- MANIFEST.in | 4 ---- setup.py | 4 ++-- src/etools/config/settings/base.py | 11 ----------- 4 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 32c4b3cf62..d61f63f4df 100644 --- a/.gitignore +++ b/.gitignore @@ -23,15 +23,11 @@ build *idea* -EquiTrack/static/* -EquiTrack/migration_errors.txt static/* media *.dump dump.rdb -.vagrant -vagrant_ansible_inventory_default *.cpg *.dbf @@ -42,7 +38,6 @@ packer_cache *.iso *.box -Vagrantfile saml/FederationMetadata.xml *.tar diff --git a/MANIFEST.in b/MANIFEST.in index 822d0c388b..2a2dd3771b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,8 +7,6 @@ include *.py recursive-include docs * recursive-include src/etools * -recursive-include src/requirements *.txt - exclude src/etools/settings/custom.*.py exclude .coverage exclude .coveragerc @@ -34,9 +32,7 @@ exclude docker-compose_v2.yml exclude docs exclude runtests.sh exclude screenshots -exclude src/requirements/input exclude wait-for-it.sh -exclude requirements.txt exclude refactor.sh exclude setup.in diff --git a/setup.py b/setup.py index 808b294b7f..080fef7b41 100644 --- a/setup.py +++ b/setup.py @@ -40,8 +40,8 @@ 'Framework :: Django', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Framework :: Django', - 'Framework :: Django :: 2.1', + 'Framework :: Django :: 2.2', ], ) diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index 83d17a9c8a..a534ae1d25 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -487,17 +487,6 @@ def before_send(event, hint): AZURE_GRAPH_API_VERSION = 'beta' AZURE_GRAPH_API_PAGE_SIZE = 250 -# drfpaswordless: https://github.com/aaronn/django-rest-framework-passwordless - -PASSWORDLESS_AUTH = { - # we can't use email here, because to_alias field length is 40, while email can be up to 254 symbols length. - # with custom user model we can avoid this a bit tricky with custom property like cropped_email, - # but for contrib user there is nothing better than use field having appropriate max length. - # username is better choice as it can be only 30 symbols max and unique. - 'PASSWORDLESS_USER_EMAIL_FIELD_NAME': 'username' -} - - KEY = os.getenv('AZURE_B2C_CLIENT_ID', None) SECRET = os.getenv('AZURE_B2C_CLIENT_SECRET', None) From 66f477fcdba8d80a7af72312c2ee8a7768b6d5b8 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Wed, 4 Sep 2019 13:32:40 -0400 Subject: [PATCH 52/56] Update unicef-attachment requirement to 0.5.2 --- Pipfile | 2 +- Pipfile.lock | 182 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 107 insertions(+), 77 deletions(-) diff --git a/Pipfile b/Pipfile index 9081b70e48..6be9caee44 100644 --- a/Pipfile +++ b/Pipfile @@ -68,7 +68,7 @@ requests = "==2.22" social-auth-app-django = "==3.1" social-auth-core = {extras = ["azuread"],version = "==3.2"} tenant-schemas-celery = "==0.2.1" -unicef_attachments = "==0.5.1" +unicef_attachments = "==0.5.2" unicef-djangolib = "==0.5.3" unicef-locations = "==1.7" unicef_notification = "==0.2.1" diff --git a/Pipfile.lock b/Pipfile.lock index 62e8fa3266..bd9e11b4eb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "abc904a56bfdc80e34ef551f3d63675da385fe10dbc9048e52b4d518cac276b7" + "sha256": "b483629161fdc06780a5628ced505836398b8829c6a7566966401ac1866d907d" }, "pipfile-spec": 6, "requires": { @@ -25,10 +25,10 @@ }, "amqp": { "hashes": [ - "sha256:aa4409446139676943a2eaa27d5f58caf750f4ca5a89f888c452afd86be6a67d", - "sha256:cbb6f87d53cac612a594f982b717cc1c54c6a1e17943a0a0d32dc6cc9e2120c8" + "sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68", + "sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a" ], - "version": "==2.5.0" + "version": "==2.5.1" }, "asn1crypto": { "hashes": [ @@ -81,10 +81,10 @@ }, "azure-datalake-store": { "hashes": [ - "sha256:10696c76716762739e5e77f25c1268314786cf8e999d6d8366af7245c82fc660", - "sha256:5bc560ee108a1824e03ab926fbe5302b84c1e8d5742fa3a31e4cc0733ab58543" + "sha256:55bcbec99a35a52ea291518dadfd94c3b645e66d05ce927228289b93979ad3a5", + "sha256:d8e72b60b52b7875c23e386af6a90a2db7f580f75627aa5ffbda18e186180d39" ], - "version": "==0.0.46" + "version": "==0.0.47" }, "azure-eventgrid": { "hashes": [ @@ -630,9 +630,10 @@ }, "billiard": { "hashes": [ - "sha256:756bf323f250db8bf88462cd042c992ba60d8f5e07fc5636c24ba7d6f4261d84" + "sha256:01afcb4e7c4fd6480940cfbd4d9edc19d7a7509d6ada533984d0d0f49901ec82", + "sha256:b8809c74f648dfe69b973c8e660bcec00603758c9db8ba89d7719f88d5f01f26" ], - "version": "==3.6.0.0" + "version": "==3.6.1.0" }, "carto": { "hashes": [ @@ -1095,6 +1096,13 @@ ], "version": "==2.8" }, + "importlib-metadata": { + "hashes": [ + "sha256:9ff1b1c5a354142de080b8a4e9803e5d0d59283c93aed808617c787d16768375", + "sha256:b7143592e374e50584564794fcb8aaf00a23025f9db866627f89a21491847a8d" + ], + "version": "==0.20" + }, "isodate": { "hashes": [ "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8", @@ -1131,37 +1139,37 @@ }, "kombu": { "hashes": [ - "sha256:55b71d3785def3470a16217fe0780f9e6f95e61bf9ad39ef8dce0177224eab77", - "sha256:eb365ea795cd7e629ba2f1f398e0c3ba354b91ef4de225ffdf6ab45fdfc7d581" + "sha256:55274dc75eb3c3994538b0973a0fadddb236b698a4bc135b8aa4981e0a710b8f", + "sha256:e5f0312dfb9011bebbf528ccaf118a6c2b5c3b8244451f08381fb23e7715809b" ], - "version": "==4.6.3" + "version": "==4.6.4" }, "lxml": { "hashes": [ - "sha256:06e5599b9c54f797a3c0f384c67705a0d621031007aa2400a6c7d17300fdb995", - "sha256:092237cfe4ece074401b75001a2e525fa6e1fb9d40fee8b7b132b1947d3bd2f8", - "sha256:0b6d49d0a26fe8207df8dd27c40b75be4deb2277173903aa76ec3e82df77cbe7", - "sha256:0f77061c20b4f32b1cf39e8f661c74e966344084c996e7b23c3a94e472461df0", - "sha256:0fef86edfa2f146b4b0ae2c6c05c3e4a8f3388b3655eafbc4aab3247f4dabb24", - "sha256:2f163c8844db4ed06a230ef092e2461ad01830972a896b8f3cf8b5bac70ae85d", - "sha256:350333190052bbfbc3222b1805b59b7979d7276e57af2257367e15a2db27082d", - "sha256:3b57dc5ed7b6a7d852c961f2389ca99404c2b59fd2088baec6fbaca02f688be4", - "sha256:3e86e5df4a8edd6f725f3c76f1d45e046d4f3aa40478092e4f5f373ad1f526e2", - "sha256:43dac60d10341d3e56be089cd0798b70e70d45ce32279f4c3190d8cbd71350e4", - "sha256:4665ee84ac8ba11d58f1ed517e29ea8536b4ae4e0c6fb6c7d3dce70abcd279f0", - "sha256:5033cf606a7cb559db967689b1b2e743994000f783607ba4c484e90917395ad7", - "sha256:75d731af05bf40f808d7716e0d26b4b02913402f861c032ce8c36efca350ae72", - "sha256:7720174604c7647e357566ac9e4d135c137caed5e7b01223551a4c81c8dc8b9a", - "sha256:b33ec641309bcea40c76c1b105f988e4e8f9a2f1ee1486aa5c0eeef33956c9bb", - "sha256:d1135dc0ac197242028ede085b693ba1f2bff7f0f9b91080e2540348312bfa53", - "sha256:d5a61e9c2322b45f259909a02b76bc98c4641214e22a37191d00c151aa9cdb9a", - "sha256:da22c4b17bc17dad9c8faf6d94c8fe568ac71c867a56631ab874da418fc7f8f7", - "sha256:da5c48ec9f8d8b5df42d328b6d1fb8d9413cd664a2367ef4f6f7cc48ee5b82c0", - "sha256:db2794bad21b7b30b6849b4e1537171cae8a7087711d958d69c233470dc612e7", - "sha256:f1c2f67df727034f94ccb590142d1d110f3dd38f638a4f1567fdd9f39892ba05", - "sha256:f840dddded8b046edc774c88ed8d2442cdb231a68894c42c74e3a809450fae76" - ], - "version": "==4.4.0" + "sha256:02ca7bf899da57084041bb0f6095333e4d239948ad3169443f454add9f4e9cb4", + "sha256:096b82c5e0ea27ce9138bcbb205313343ee66a6e132f25c5ed67e2c8d960a1bc", + "sha256:0a920ff98cf1aac310470c644bc23b326402d3ef667ddafecb024e1713d485f1", + "sha256:17cae1730a782858a6e2758fd20dd0ef7567916c47757b694a06ffafdec20046", + "sha256:17e3950add54c882e032527795c625929613adbd2ce5162b94667334458b5a36", + "sha256:1f4f214337f6ee5825bf90a65d04d70aab05526c08191ab888cb5149501923c5", + "sha256:2e8f77db25b0a96af679e64ff9bf9dddb27d379c9900c3272f3041c4d1327c9d", + "sha256:4dffd405390a45ecb95ab5ab1c1b847553c18b0ef8ed01e10c1c8b1a76452916", + "sha256:6b899931a5648862c7b88c795eddff7588fb585e81cecce20f8d9da16eff96e0", + "sha256:726c17f3e0d7a7200718c9a890ccfeab391c9133e363a577a44717c85c71db27", + "sha256:760c12276fee05c36f95f8040180abc7fbebb9e5011447a97cdc289b5d6ab6fc", + "sha256:796685d3969815a633827c818863ee199440696b0961e200b011d79b9394bbe7", + "sha256:891fe897b49abb7db470c55664b198b1095e4943b9f82b7dcab317a19116cd38", + "sha256:a471628e20f03dcdfde00770eeaf9c77811f0c331c8805219ca7b87ac17576c5", + "sha256:a63b4fd3e2cabdcc9d918ed280bdde3e8e9641e04f3c59a2a3109644a07b9832", + "sha256:b0b84408d4eabc6de9dd1e1e0bc63e7731e890c0b378a62443e5741cfd0ae90a", + "sha256:be78485e5d5f3684e875dab60f40cddace2f5b2a8f7fede412358ab3214c3a6f", + "sha256:c27eaed872185f047bb7f7da2d21a7d8913457678c9a100a50db6da890bc28b9", + "sha256:c81cb40bff373ab7a7446d6bbca0190bccc5be3448b47b51d729e37799bb5692", + "sha256:d11874b3c33ee441059464711cd365b89fa1a9cf19ae75b0c189b01fbf735b84", + "sha256:e9c028b5897901361d81a4718d1db217b716424a0283afe9d6735fe0caf70f79", + "sha256:fe489d486cd00b739be826e8c1be188ddb74c7a1ca784d93d06fda882a6a1681" + ], + "version": "==4.4.1" }, "markupsafe": { "hashes": [ @@ -1196,6 +1204,13 @@ ], "version": "==1.1.1" }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, "msrest": { "hashes": [ "sha256:27589fb400da7e1a98778688f70a0099e4fc6fea59d0f4835b4fbdad3bb8a6d9", @@ -1219,10 +1234,10 @@ }, "oauthlib": { "hashes": [ - "sha256:40a63637707e9163eda62d0f5345120c65e001a790480b8256448543c1f78f66", - "sha256:b4d99ae8ccfb7d33ba9591b59355c64eef5241534aa3da2e4c0435346b84bc8e" + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], - "version": "==3.0.2" + "version": "==3.1.0" }, "odfpy": { "hashes": [ @@ -1238,9 +1253,9 @@ }, "openpyxl": { "hashes": [ - "sha256:1d2af392cef8c8227bd2ac3ebe3a28b25aba74fd4fa473ce106065f0b73bfe2e" + "sha256:72d1ed243972cad0b3c236230083cac00d9c72804e64a2ae93d7901aec1a8f1c" ], - "version": "==2.6.2" + "version": "==2.6.3" }, "pillow": { "hashes": [ @@ -1415,10 +1430,10 @@ }, "redis": { "hashes": [ - "sha256:8106502b96280e8614d30766f28b4e07ca2b368a8d7896bffce9de88756dd481", - "sha256:95ccbec607e21fff2823e7da8b7041e0c471eb36d0672f20abcb32adb5b089ca" + "sha256:98a22fb750c9b9bb46e75e945dc3f61d0ab30d06117cbb21ff9cd1d315fedd3b", + "sha256:c504251769031b0dd7dd5cf786050a6050197c6de0d37778c80c08cb04ae8275" ], - "version": "==3.3.5" + "version": "==3.3.8" }, "reportlab": { "hashes": [ @@ -1562,10 +1577,11 @@ }, "unicef-attachments": { "hashes": [ - "sha256:6d6bd2e606caf06d2b78762ab5319e6f39d569f123e665473e23a874669b8a7f" + "sha256:59989da40ae709a85de62894acd6b20b71b90c84bfc1f4b5ce22be2b5bf71106", + "sha256:edfebbc162db535b4394a0a5784dad444a14224a584227f3293da12804cff2bb" ], "index": "pypi", - "version": "==0.5.1" + "version": "==0.5.2" }, "unicef-djangolib": { "hashes": [ @@ -1673,6 +1689,13 @@ "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" ], "version": "==1.3.0" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" } }, "develop": { @@ -1781,10 +1804,10 @@ }, "drf-api-checker": { "hashes": [ - "sha256:db4045afe4585120ab298464c37887d8567a0ecadbe8eaa6a0e348c364d745bb" + "sha256:606861d85a3f0bfda7b3789a6bc4ed1c92724662d2e0756b5a2bb9841a21d4df" ], "index": "pypi", - "version": "==0.4.1" + "version": "==0.7.0" }, "entrypoints": { "hashes": [ @@ -1803,10 +1826,10 @@ }, "faker": { "hashes": [ - "sha256:96ad7902706f2409a2d0c3de5132f69b413555a419bacec99d3f16e657895b47", - "sha256:b3bb64aff9571510de6812df45122b633dbc6227e870edae3ed9430f94698521" + "sha256:1d3f700e8dfcefd6e657118d71405d53e86974448aba78884f119bbd84c0cddf", + "sha256:d5366e120191c5610fceeebfe1c298dc46da0277096f639c6dd7e2eaee0fa547" ], - "version": "==2.0.0" + "version": "==2.0.1" }, "fancycompleter": { "hashes": [ @@ -1853,18 +1876,18 @@ }, "importlib-metadata": { "hashes": [ - "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", - "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" + "sha256:9ff1b1c5a354142de080b8a4e9803e5d0d59283c93aed808617c787d16768375", + "sha256:b7143592e374e50584564794fcb8aaf00a23025f9db866627f89a21491847a8d" ], - "version": "==0.19" + "version": "==0.20" }, "ipython": { "hashes": [ - "sha256:1d3a1692921e932751bc1a1f7bb96dc38671eeefdc66ed33ee4cbc57e92a410e", - "sha256:537cd0176ff6abd06ef3e23f2d0c4c2c8a4d9277b7451544c6cbf56d1c79a83d" + "sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b", + "sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1" ], "index": "pypi", - "version": "==7.7.0" + "version": "==7.8.0" }, "ipython-genutils": { "hashes": [ @@ -1883,10 +1906,10 @@ }, "jedi": { "hashes": [ - "sha256:53c850f1a7d3cfcd306cc513e2450a54bdf5cacd7604b74e42dd1f0758eaaf36", - "sha256:e07457174ef7cb2342ff94fa56484fe41cec7ef69b0059f01d3f812379cb6f7c" + "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", + "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e" ], - "version": "==0.14.1" + "version": "==0.15.1" }, "jinja2": { "hashes": [ @@ -1943,6 +1966,13 @@ "index": "pypi", "version": "==3.0.5" }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, "multidict": { "hashes": [ "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", @@ -2133,11 +2163,11 @@ }, "sphinx": { "hashes": [ - "sha256:22538e1bbe62b407cf5a8aabe1bb15848aa66bb79559f42f5202bbce6b757a69", - "sha256:f9a79e746b87921cabc3baa375199c6076d1270cee53915dbd24fdbeaaacc427" + "sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845", + "sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069" ], "index": "pypi", - "version": "==2.1.2" + "version": "==2.2.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -2197,11 +2227,11 @@ }, "tox": { "hashes": [ - "sha256:dab0b0160dd187b654fc33d690ee1d7bf328bd5b8dc6ef3bb3cc468969c659ba", - "sha256:ee35ffce74933a6c6ac10c9a0182e41763140a5a5070e21b114feca56eaccdcd" + "sha256:0bc216b6a2e6afe764476b4a07edf2c1dab99ed82bb146a1130b2e828f5bff5e", + "sha256:c4f6b319c20ba4913dbfe71ebfd14ff95d1853c4231493608182f66e566ecfe1" ], "index": "pypi", - "version": "==3.13.2" + "version": "==3.14.0" }, "traitlets": { "hashes": [ @@ -2219,18 +2249,18 @@ }, "vcrpy": { "hashes": [ - "sha256:127e79cf7b569d071d1bd761b83f7b62b2ce2a2eb63ceca7aa67cba8f2602ea3", - "sha256:57be64aa8e9883a4117d0b15de28af62275c001abcdb00b6dc2d4406073d9a4f" + "sha256:0e79239441fb4c731da9f05aecbd062223ef1f4ab668d2400c63c347a7071414", + "sha256:b5bcdd4450d03acd623515c8b9fbf3f1f7fa3a44fd0c01c674b0f1166a1949f8" ], "index": "pypi", - "version": "==2.0.1" + "version": "==2.1.0" }, "virtualenv": { "hashes": [ - "sha256:6cb2e4c18d22dbbe283d0a0c31bb7d90771a606b2cb3415323eea008eaee6a9d", - "sha256:909fe0d3f7c9151b2df0a2cb53e55bdb7b0d61469353ff7a49fd47b0f0ab9285" + "sha256:680af46846662bb38c5504b78bad9ed9e4f3ba2d54f54ba42494fdf94337fe30", + "sha256:f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2" ], - "version": "==16.7.2" + "version": "==16.7.5" }, "wcwidth": { "hashes": [ @@ -2265,15 +2295,15 @@ "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" ], - "markers": "python_version >= '3.4'", + "markers": "python_version >= '3.5'", "version": "==1.3.0" }, "zipp": { "hashes": [ - "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", - "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" ], - "version": "==0.5.2" + "version": "==0.6.0" } } } From 04e8f7d931f9c3b389a38c9e9d5c86f09a3cc08d Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 5 Sep 2019 07:29:18 -0400 Subject: [PATCH 53/56] Update pipfile lock hashes --- Pipfile.lock | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index bd9e11b4eb..8eea242965 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1213,10 +1213,10 @@ }, "msrest": { "hashes": [ - "sha256:27589fb400da7e1a98778688f70a0099e4fc6fea59d0f4835b4fbdad3bb8a6d9", - "sha256:cda706a2ccfb032cf41fa8cc6575cbca29634fed2d226fc789e4a8daf44ab7c1" + "sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d", + "sha256:f5153bfe60ee757725816aedaa0772cbfe0bddb52cd2d6db4cb8b4c3c6c6f928" ], - "version": "==0.6.9" + "version": "==0.6.10" }, "msrestazure": { "hashes": [ @@ -1577,8 +1577,7 @@ }, "unicef-attachments": { "hashes": [ - "sha256:59989da40ae709a85de62894acd6b20b71b90c84bfc1f4b5ce22be2b5bf71106", - "sha256:edfebbc162db535b4394a0a5784dad444a14224a584227f3293da12804cff2bb" + "sha256:80c60906bb9b5e86b8eff86f9b3a702166e7fcd5681dd864b1d651a98345cf62" ], "index": "pypi", "version": "==0.5.2" From a5df066f19f6262540bed972858c4bd53d1dbe3d Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 5 Sep 2019 10:38:13 -0400 Subject: [PATCH 54/56] 14494 fixed export --- src/etools/applications/hact/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/hact/views.py b/src/etools/applications/hact/views.py index 5de7241bdc..ded38abf11 100644 --- a/src/etools/applications/hact/views.py +++ b/src/etools/applications/hact/views.py @@ -86,8 +86,8 @@ def get(self, request, *args, **kwargs): partner_values['assurance_activities']['programmatic_visits']['min_required']), ('Assessment and Assurance Activities', 'Spot Checks: Completed', partner_values['assurance_activities']['spot_checks']['completed']), - ('Assessment and Assurance Activities', 'Spot Checks: Minimum Required', - partner_values['assurance_activities']['spot_checks']['min_required']), + ('Assessment and Assurance Activities', 'Spot Checks: Required', + partner_values['assurance_activities']['spot_checks']['required']), ('Assessment and Assurance Activities', 'Micro Assessments: Completed', partner_values['assurance_activities']['micro_assessment']), ('Assessment and Assurance Activities', 'Micro Assessments: Expiring', From 85636cafcefa3fe554731314a53c84efda835b27 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 5 Sep 2019 17:01:24 -0400 Subject: [PATCH 55/56] Prp sync changes discussed --- src/etools/applications/partners/views/prp_v1.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/partners/views/prp_v1.py b/src/etools/applications/partners/views/prp_v1.py index c65ef02dec..f92574a06b 100644 --- a/src/etools/applications/partners/views/prp_v1.py +++ b/src/etools/applications/partners/views/prp_v1.py @@ -48,7 +48,12 @@ class PRPInterventionListAPIView(QueryStringFilterMixin, ListAPIView): permission_classes = (ListCreateAPIMixedPermission, ) filter_backends = (PartnerScopeFilter,) pagination_class = PRPInterventionPagination - queryset = Intervention.objects.prefetch_related( + + queryset = Intervention.objects.filter( + result_links__ll_results__applied_indicators__id__isnull=False, + reporting_requirements__id__isnull=False, + in_amendment=False + ).prefetch_related( 'result_links__cp_output', 'result_links__ll_results', 'result_links__ll_results__applied_indicators__indicator', From f5b907cb02d6fe02861b42bc305d2ece6391777d Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 5 Sep 2019 20:42:39 -0400 Subject: [PATCH 56/56] fix tests --- .../TestAPIPRP/fixtures.json | 2382 +++++++++++++++++ ...ntions__{'workspace': 'ZZZ'}.response.json | 143 + .../partners/tests/test_api_prp.py | 100 +- .../applications/partners/tests/test_utils.py | 8 +- .../applications/partners/views/prp_v1.py | 4 +- 5 files changed, 2629 insertions(+), 8 deletions(-) create mode 100644 src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/fixtures.json create mode 100644 src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/get__api_prp_v1_interventions__{'workspace': 'ZZZ'}.response.json diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/fixtures.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/fixtures.json new file mode 100644 index 0000000000..9d7c2db94b --- /dev/null +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/fixtures.json @@ -0,0 +1,2382 @@ +{ + "unicef_staff": { + "master": { + "model": "users.user", + "pk": 6664, + "fields": { + "created": "2019-09-06T09:50:26.851Z", + "modified": "2019-09-06T09:50:26.862Z", + "username": "QvnLzWzvHUSg", + "email": "staff@unicef.org", + "password": "md5$EcJak3vWmN4B$97409c09f89ec7a732d1938241774a34", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.851Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410 + ], + "user_permissions": [] + } + }, + "deps": [ + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + } + ] + }, + "partnership_manager_user": { + "master": { + "model": "users.user", + "pk": 6663, + "fields": { + "created": "2019-09-06T09:50:26.785Z", + "modified": "2019-09-06T09:50:26.801Z", + "username": "HdBYgeVGdsOP", + "email": "partner@unicef.org", + "password": "md5$5iBjdXIbedGz$8c4962c3a7a2637d84054af33c0ba9f7", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.785Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410, + 411 + ], + "user_permissions": [] + } + }, + "deps": [ + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + }, + { + "model": "auth.group", + "pk": 411, + "fields": { + "name": "Partnership Manager", + "permissions": [] + } + } + ] + }, + "partner": { + "master": { + "model": "partners.partnerorganization", + "pk": 2014, + "fields": { + "created": "2019-09-06T09:50:26.805Z", + "modified": "2019-09-06T09:50:26.809Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 1", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": "VP1", + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + "deps": [] + }, + "partner1": { + "master": { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + "deps": [] + }, + "agreement": { + "master": { + "model": "partners.agreement", + "pk": 1979, + "fields": { + "created": "2019-09-06T09:50:26.816Z", + "modified": "2019-09-06T09:50:26.839Z", + "partner": 2014, + "country_programme": 1979, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191979", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "signed", + "authorized_officers": [] + } + }, + "deps": [ + { + "model": "partners.partnerorganization", + "pk": 2014, + "fields": { + "created": "2019-09-06T09:50:26.805Z", + "modified": "2019-09-06T09:50:26.809Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 1", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": "VP1", + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1979, + "fields": { + "name": "Country Programme 0", + "wbs": "0000/A0/00", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + } + ] + }, + "active_agreement": { + "master": { + "model": "partners.agreement", + "pk": 1980, + "fields": { + "created": "2019-09-06T09:50:26.840Z", + "modified": "2019-09-06T09:50:26.850Z", + "partner": 2015, + "country_programme": 1980, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191980", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "active", + "authorized_officers": [] + } + }, + "deps": [ + { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1980, + "fields": { + "name": "Country Programme 1", + "wbs": "0000/A0/01", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + } + ] + }, + "intervention": { + "master": { + "model": "partners.intervention", + "pk": 2044, + "fields": { + "created": "2019-09-06T09:50:26.864Z", + "modified": "2019-09-06T09:50:26.867Z", + "document_type": "", + "agreement": 1979, + "country_programme": null, + "number": "TST/PCA20191979/20192044", + "title": "Intervention 1", + "status": "draft", + "start": null, + "end": null, + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": null, + "signed_by_partner_date": null, + "unicef_signatory": null, + "partner_authorized_officer_signatory": null, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + "deps": [ + { + "model": "partners.agreement", + "pk": 1979, + "fields": { + "created": "2019-09-06T09:50:26.816Z", + "modified": "2019-09-06T09:50:26.839Z", + "partner": 2014, + "country_programme": 1979, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191979", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "signed", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2014, + "fields": { + "created": "2019-09-06T09:50:26.805Z", + "modified": "2019-09-06T09:50:26.809Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 1", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": "VP1", + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1979, + "fields": { + "name": "Country Programme 0", + "wbs": "0000/A0/00", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + } + ] + }, + "intervention_2": { + "master": { + "model": "partners.intervention", + "pk": 2046, + "fields": { + "created": "2019-09-06T09:50:26.915Z", + "modified": "2019-09-06T09:50:26.917Z", + "document_type": "PD", + "agreement": 1979, + "country_programme": null, + "number": "TST/PCA20191979/PD20192046", + "title": "Intervention 2", + "status": "draft", + "start": null, + "end": null, + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": null, + "signed_by_partner_date": null, + "unicef_signatory": null, + "partner_authorized_officer_signatory": null, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + "deps": [ + { + "model": "partners.agreement", + "pk": 1979, + "fields": { + "created": "2019-09-06T09:50:26.816Z", + "modified": "2019-09-06T09:50:26.839Z", + "partner": 2014, + "country_programme": 1979, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191979", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "signed", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2014, + "fields": { + "created": "2019-09-06T09:50:26.805Z", + "modified": "2019-09-06T09:50:26.809Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 1", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": "VP1", + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1979, + "fields": { + "name": "Country Programme 0", + "wbs": "0000/A0/00", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + } + ] + }, + "active_intervention": { + "master": { + "model": "partners.intervention", + "pk": 2045, + "fields": { + "created": "2019-09-06T09:50:26.872Z", + "modified": "2019-09-06T09:50:26.874Z", + "document_type": "PD", + "agreement": 1980, + "country_programme": null, + "number": "TST/PCA20191980/PD20192045", + "title": "Active Intervention", + "status": "active", + "start": "2019-09-05", + "end": "2019-12-05", + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": "2019-09-05", + "signed_by_partner_date": "2019-09-05", + "unicef_signatory": 6664, + "partner_authorized_officer_signatory": 2015, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + "deps": [ + { + "model": "partners.agreement", + "pk": 1980, + "fields": { + "created": "2019-09-06T09:50:26.840Z", + "modified": "2019-09-06T09:50:26.850Z", + "partner": 2015, + "country_programme": 1980, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191980", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "active", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1980, + "fields": { + "name": "Country Programme 1", + "wbs": "0000/A0/01", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + }, + { + "model": "users.user", + "pk": 6664, + "fields": { + "created": "2019-09-06T09:50:26.851Z", + "modified": "2019-09-06T09:50:26.862Z", + "username": "QvnLzWzvHUSg", + "email": "staff@unicef.org", + "password": "md5$EcJak3vWmN4B$97409c09f89ec7a732d1938241774a34", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.851Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410 + ], + "user_permissions": [] + } + }, + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + }, + { + "model": "partners.partnerstaffmember", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.812Z", + "modified": "2019-09-06T09:50:26.812Z", + "partner": 2015, + "title": "Jedi Master", + "first_name": "Mace", + "last_name": "Windu", + "email": "mace1@example.com", + "phone": "", + "active": true + } + } + ] + }, + "reporting_requirement": { + "master": { + "model": "reports.reportingrequirement", + "pk": 40, + "fields": { + "created": "2019-09-06T09:50:26.919Z", + "modified": "2019-09-06T09:50:26.919Z", + "intervention": 2045, + "start_date": "2001-01-29", + "end_date": "2010-04-07", + "due_date": "2018-05-28", + "report_type": "('HR', 'Humanitarian Report')" + } + }, + "deps": [ + { + "model": "partners.intervention", + "pk": 2045, + "fields": { + "created": "2019-09-06T09:50:26.872Z", + "modified": "2019-09-06T09:50:26.874Z", + "document_type": "PD", + "agreement": 1980, + "country_programme": null, + "number": "TST/PCA20191980/PD20192045", + "title": "Active Intervention", + "status": "active", + "start": "2019-09-05", + "end": "2019-12-05", + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": "2019-09-05", + "signed_by_partner_date": "2019-09-05", + "unicef_signatory": 6664, + "partner_authorized_officer_signatory": 2015, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + { + "model": "partners.agreement", + "pk": 1980, + "fields": { + "created": "2019-09-06T09:50:26.840Z", + "modified": "2019-09-06T09:50:26.850Z", + "partner": 2015, + "country_programme": 1980, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191980", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "active", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1980, + "fields": { + "name": "Country Programme 1", + "wbs": "0000/A0/01", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + }, + { + "model": "users.user", + "pk": 6664, + "fields": { + "created": "2019-09-06T09:50:26.851Z", + "modified": "2019-09-06T09:50:26.862Z", + "username": "QvnLzWzvHUSg", + "email": "staff@unicef.org", + "password": "md5$EcJak3vWmN4B$97409c09f89ec7a732d1938241774a34", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.851Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410 + ], + "user_permissions": [] + } + }, + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + }, + { + "model": "partners.partnerstaffmember", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.812Z", + "modified": "2019-09-06T09:50:26.812Z", + "partner": 2015, + "title": "Jedi Master", + "first_name": "Mace", + "last_name": "Windu", + "email": "mace1@example.com", + "phone": "", + "active": true + } + } + ] + }, + "result_type": { + "master": { + "model": "reports.resulttype", + "pk": 1871, + "fields": { + "name": "Output" + } + }, + "deps": [] + }, + "result": { + "master": { + "model": "reports.result", + "pk": 1871, + "fields": { + "country_programme": null, + "result_type": 1871, + "sector": null, + "name": "Result 0", + "code": null, + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "parent": null, + "humanitarian_tag": false, + "humanitarian_marker_code": null, + "humanitarian_marker_name": null, + "wbs": null, + "vision_id": null, + "gic_code": null, + "gic_name": null, + "sic_code": null, + "sic_name": null, + "activity_focus_code": null, + "activity_focus_name": null, + "hidden": false, + "ram": false, + "created": "2019-09-06T09:50:26.876Z", + "modified": "2019-09-06T09:50:26.876Z", + "lft": 1, + "rght": 2, + "tree_id": 1, + "level": 0 + } + }, + "deps": [ + { + "model": "reports.resulttype", + "pk": 1871, + "fields": { + "name": "Output" + } + } + ] + }, + "partnership_budget": { + "master": { + "model": "partners.interventionbudget", + "pk": 52, + "fields": { + "created": "2019-09-06T09:50:26.919Z", + "modified": "2019-09-06T09:50:26.919Z", + "intervention": 2044, + "partner_contribution": "20.00", + "unicef_cash": "10.00", + "in_kind_amount": "0.00", + "total": "30.00", + "partner_contribution_local": "200.00", + "unicef_cash_local": "100.00", + "in_kind_amount_local": "10.00", + "currency": "", + "total_local": "310.00" + } + }, + "deps": [ + { + "model": "partners.intervention", + "pk": 2044, + "fields": { + "created": "2019-09-06T09:50:26.864Z", + "modified": "2019-09-06T09:50:26.867Z", + "document_type": "", + "agreement": 1979, + "country_programme": null, + "number": "TST/PCA20191979/20192044", + "title": "Intervention 1", + "status": "draft", + "start": null, + "end": null, + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": null, + "signed_by_partner_date": null, + "unicef_signatory": null, + "partner_authorized_officer_signatory": null, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + { + "model": "partners.agreement", + "pk": 1979, + "fields": { + "created": "2019-09-06T09:50:26.816Z", + "modified": "2019-09-06T09:50:26.839Z", + "partner": 2014, + "country_programme": 1979, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191979", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "signed", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2014, + "fields": { + "created": "2019-09-06T09:50:26.805Z", + "modified": "2019-09-06T09:50:26.809Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 1", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": "VP1", + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1979, + "fields": { + "name": "Country Programme 0", + "wbs": "0000/A0/00", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + } + ] + }, + "fr_1": { + "master": { + "model": "funds.fundsreservationheader", + "pk": 99, + "fields": { + "created": "2019-09-06T09:50:26.921Z", + "modified": "2019-09-06T09:50:26.921Z", + "intervention": null, + "vendor_code": "YuqEOQMFqloxYHrvYjjV", + "fr_number": "qXHQBdgDZdhIYtxNTTtl", + "document_date": "2019-01-01", + "fr_type": "DrCdefpczGOveCknefML", + "currency": "USD", + "document_text": "WKxSRgRxeDYnHtafxIAv", + "intervention_amt": "67.25", + "total_amt": "122.35", + "total_amt_local": "125.88", + "actual_amt": "244.74", + "actual_amt_local": "297.71", + "outstanding_amt": "257.71", + "outstanding_amt_local": "154.74", + "start_date": "2018-12-30", + "end_date": "2020-01-07", + "multi_curr_flag": false, + "completed_flag": false, + "delegated": false + } + }, + "deps": [] + }, + "fr_2": { + "master": { + "model": "funds.fundsreservationheader", + "pk": 100, + "fields": { + "created": "2019-09-06T09:50:26.923Z", + "modified": "2019-09-06T09:50:26.923Z", + "intervention": null, + "vendor_code": "QGixIDUqaperZjhalZle", + "fr_number": "cNgFvaJxSSdeZaaVvffu", + "document_date": "2019-01-01", + "fr_type": "IPAPKYpFMShqPUixmwga", + "currency": "USD", + "document_text": "hZJkBAWDMzouymlggpSz", + "intervention_amt": "210.75", + "total_amt": "35.80", + "total_amt_local": "293.79", + "actual_amt": "274.72", + "actual_amt_local": "245.28", + "outstanding_amt": "166.94", + "outstanding_amt_local": "167.80", + "start_date": "2018-12-30", + "end_date": "2020-01-11", + "multi_curr_flag": false, + "completed_flag": false, + "delegated": false + } + }, + "deps": [] + }, + "result_link": { + "master": { + "model": "partners.interventionresultlink", + "pk": 1693, + "fields": { + "created": "2019-09-06T09:50:26.878Z", + "modified": "2019-09-06T09:50:26.878Z", + "intervention": 2045, + "cp_output": 1871, + "ram_indicators": [] + } + }, + "deps": [ + { + "model": "partners.intervention", + "pk": 2045, + "fields": { + "created": "2019-09-06T09:50:26.872Z", + "modified": "2019-09-06T09:50:26.874Z", + "document_type": "PD", + "agreement": 1980, + "country_programme": null, + "number": "TST/PCA20191980/PD20192045", + "title": "Active Intervention", + "status": "active", + "start": "2019-09-05", + "end": "2019-12-05", + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": "2019-09-05", + "signed_by_partner_date": "2019-09-05", + "unicef_signatory": 6664, + "partner_authorized_officer_signatory": 2015, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + { + "model": "partners.agreement", + "pk": 1980, + "fields": { + "created": "2019-09-06T09:50:26.840Z", + "modified": "2019-09-06T09:50:26.850Z", + "partner": 2015, + "country_programme": 1980, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191980", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "active", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1980, + "fields": { + "name": "Country Programme 1", + "wbs": "0000/A0/01", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + }, + { + "model": "users.user", + "pk": 6664, + "fields": { + "created": "2019-09-06T09:50:26.851Z", + "modified": "2019-09-06T09:50:26.862Z", + "username": "QvnLzWzvHUSg", + "email": "staff@unicef.org", + "password": "md5$EcJak3vWmN4B$97409c09f89ec7a732d1938241774a34", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.851Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410 + ], + "user_permissions": [] + } + }, + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + }, + { + "model": "partners.partnerstaffmember", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.812Z", + "modified": "2019-09-06T09:50:26.812Z", + "partner": 2015, + "title": "Jedi Master", + "first_name": "Mace", + "last_name": "Windu", + "email": "mace1@example.com", + "phone": "", + "active": true + } + }, + { + "model": "reports.result", + "pk": 1871, + "fields": { + "country_programme": null, + "result_type": 1871, + "sector": null, + "name": "Result 0", + "code": null, + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "parent": null, + "humanitarian_tag": false, + "humanitarian_marker_code": null, + "humanitarian_marker_name": null, + "wbs": null, + "vision_id": null, + "gic_code": null, + "gic_name": null, + "sic_code": null, + "sic_name": null, + "activity_focus_code": null, + "activity_focus_name": null, + "hidden": false, + "ram": false, + "created": "2019-09-06T09:50:26.876Z", + "modified": "2019-09-06T09:50:26.876Z", + "lft": 1, + "rght": 2, + "tree_id": 1, + "level": 0 + } + }, + { + "model": "reports.resulttype", + "pk": 1871, + "fields": { + "name": "Output" + } + } + ] + }, + "lower_result": { + "master": { + "model": "reports.lowerresult", + "pk": 109, + "fields": { + "created": "2019-09-06T09:50:26.880Z", + "modified": "2019-09-06T09:50:26.880Z", + "result_link": 1693, + "name": "Lower Result 1", + "code": "2045-1" + } + }, + "deps": [ + { + "model": "partners.interventionresultlink", + "pk": 1693, + "fields": { + "created": "2019-09-06T09:50:26.878Z", + "modified": "2019-09-06T09:50:26.878Z", + "intervention": 2045, + "cp_output": 1871, + "ram_indicators": [] + } + }, + { + "model": "partners.intervention", + "pk": 2045, + "fields": { + "created": "2019-09-06T09:50:26.872Z", + "modified": "2019-09-06T09:50:26.874Z", + "document_type": "PD", + "agreement": 1980, + "country_programme": null, + "number": "TST/PCA20191980/PD20192045", + "title": "Active Intervention", + "status": "active", + "start": "2019-09-05", + "end": "2019-12-05", + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": "2019-09-05", + "signed_by_partner_date": "2019-09-05", + "unicef_signatory": 6664, + "partner_authorized_officer_signatory": 2015, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + { + "model": "partners.agreement", + "pk": 1980, + "fields": { + "created": "2019-09-06T09:50:26.840Z", + "modified": "2019-09-06T09:50:26.850Z", + "partner": 2015, + "country_programme": 1980, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191980", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "active", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1980, + "fields": { + "name": "Country Programme 1", + "wbs": "0000/A0/01", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + }, + { + "model": "users.user", + "pk": 6664, + "fields": { + "created": "2019-09-06T09:50:26.851Z", + "modified": "2019-09-06T09:50:26.862Z", + "username": "QvnLzWzvHUSg", + "email": "staff@unicef.org", + "password": "md5$EcJak3vWmN4B$97409c09f89ec7a732d1938241774a34", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.851Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410 + ], + "user_permissions": [] + } + }, + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + }, + { + "model": "partners.partnerstaffmember", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.812Z", + "modified": "2019-09-06T09:50:26.812Z", + "partner": 2015, + "title": "Jedi Master", + "first_name": "Mace", + "last_name": "Windu", + "email": "mace1@example.com", + "phone": "", + "active": true + } + }, + { + "model": "reports.result", + "pk": 1871, + "fields": { + "country_programme": null, + "result_type": 1871, + "sector": null, + "name": "Result 0", + "code": null, + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "parent": null, + "humanitarian_tag": false, + "humanitarian_marker_code": null, + "humanitarian_marker_name": null, + "wbs": null, + "vision_id": null, + "gic_code": null, + "gic_name": null, + "sic_code": null, + "sic_name": null, + "activity_focus_code": null, + "activity_focus_name": null, + "hidden": false, + "ram": false, + "created": "2019-09-06T09:50:26.876Z", + "modified": "2019-09-06T09:50:26.876Z", + "lft": 1, + "rght": 2, + "tree_id": 1, + "level": 0 + } + }, + { + "model": "reports.resulttype", + "pk": 1871, + "fields": { + "name": "Output" + } + } + ] + }, + "indicator_blueprint": { + "master": { + "model": "reports.indicatorblueprint", + "pk": 109, + "fields": { + "created": "2019-09-06T09:50:26.879Z", + "modified": "2019-09-06T09:50:26.879Z", + "title": "The Blueprint", + "unit": "number", + "description": null, + "code": null, + "subdomain": null, + "disaggregatable": false, + "calculation_formula_across_periods": "sum", + "calculation_formula_across_locations": "sum", + "display_type": "number" + } + }, + "deps": [] + }, + "applied_indicator": { + "master": { + "model": "reports.appliedindicator", + "pk": 108, + "fields": { + "created": "2019-09-06T09:50:26.882Z", + "modified": "2019-09-06T09:50:26.882Z", + "indicator": 109, + "measurement_specifications": null, + "label": null, + "numerator_label": null, + "denominator_label": null, + "section": null, + "cluster_indicator_id": null, + "response_plan_name": null, + "cluster_name": null, + "cluster_indicator_title": null, + "lower_result": 109, + "context_code": null, + "target": { + "d": 1, + "v": 0 + }, + "baseline": { + "d": 1, + "v": 0 + }, + "assumptions": null, + "means_of_verification": null, + "total": 0, + "is_high_frequency": false, + "is_active": true, + "disaggregation": [ + 70 + ], + "locations": [ + 1893 + ] + } + }, + "deps": [ + { + "model": "reports.indicatorblueprint", + "pk": 109, + "fields": { + "created": "2019-09-06T09:50:26.879Z", + "modified": "2019-09-06T09:50:26.879Z", + "title": "The Blueprint", + "unit": "number", + "description": null, + "code": null, + "subdomain": null, + "disaggregatable": false, + "calculation_formula_across_periods": "sum", + "calculation_formula_across_locations": "sum", + "display_type": "number" + } + }, + { + "model": "reports.lowerresult", + "pk": 109, + "fields": { + "created": "2019-09-06T09:50:26.880Z", + "modified": "2019-09-06T09:50:26.880Z", + "result_link": 1693, + "name": "Lower Result 1", + "code": "2045-1" + } + }, + { + "model": "partners.interventionresultlink", + "pk": 1693, + "fields": { + "created": "2019-09-06T09:50:26.878Z", + "modified": "2019-09-06T09:50:26.878Z", + "intervention": 2045, + "cp_output": 1871, + "ram_indicators": [] + } + }, + { + "model": "partners.intervention", + "pk": 2045, + "fields": { + "created": "2019-09-06T09:50:26.872Z", + "modified": "2019-09-06T09:50:26.874Z", + "document_type": "PD", + "agreement": 1980, + "country_programme": null, + "number": "TST/PCA20191980/PD20192045", + "title": "Active Intervention", + "status": "active", + "start": "2019-09-05", + "end": "2019-12-05", + "submission_date": "2019-09-06", + "submission_date_prc": null, + "reference_number_year": 2019, + "review_date_prc": null, + "prc_review_document": "", + "signed_pd_document": "", + "signed_by_unicef_date": "2019-09-05", + "signed_by_partner_date": "2019-09-05", + "unicef_signatory": 6664, + "partner_authorized_officer_signatory": 2015, + "contingency_pd": false, + "activation_letter": "", + "termination_doc": "", + "population_focus": null, + "in_amendment": false, + "metadata": {}, + "unicef_focal_points": [], + "partner_focal_points": [], + "sections": [], + "offices": [], + "flat_locations": [] + } + }, + { + "model": "partners.agreement", + "pk": 1980, + "fields": { + "created": "2019-09-06T09:50:26.840Z", + "modified": "2019-09-06T09:50:26.850Z", + "partner": 2015, + "country_programme": 1980, + "agreement_type": "PCA", + "agreement_number": "TST/PCA20191980", + "attached_agreement": "", + "start": "2019-09-06", + "end": "2019-12-31", + "reference_number_year": 2019, + "special_conditions_pca": false, + "signed_by_unicef_date": "2019-09-06", + "signed_by": null, + "signed_by_partner_date": "2019-09-06", + "partner_manager": null, + "status": "active", + "authorized_officers": [] + } + }, + { + "model": "partners.partnerorganization", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.810Z", + "modified": "2019-09-06T09:50:26.813Z", + "partner_type": "", + "cso_type": null, + "name": "Partner 2", + "short_name": "", + "description": "", + "shared_with": null, + "street_address": null, + "city": null, + "postal_code": null, + "country": null, + "address": null, + "email": null, + "phone_number": null, + "vendor_number": null, + "alternate_id": null, + "alternate_name": null, + "rating": null, + "type_of_assessment": null, + "last_assessment_date": null, + "core_values_assessment_date": null, + "vision_synced": false, + "blocked": false, + "deleted_flag": false, + "manually_blocked": false, + "hidden": false, + "total_ct_cp": null, + "total_ct_cy": null, + "net_ct_cy": null, + "reported_cy": null, + "total_ct_ytd": null, + "outstanding_dct_amount_6_to_9_months_usd": null, + "outstanding_dct_amount_more_than_9_months_usd": null, + "hact_values": { + "audits": { + "completed": 0, + "minimum_requirements": 0 + }, + "spot_checks": { + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "follow_up_required": 0 + }, + "assurance_coverage": "void", + "programmatic_visits": { + "planned": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + }, + "completed": { + "q1": 0, + "q2": 0, + "q3": 0, + "q4": 0, + "total": 0 + } + }, + "outstanding_findings": 0 + }, + "basis_for_risk_rating": "" + } + }, + { + "model": "reports.countryprogramme", + "pk": 1980, + "fields": { + "name": "Country Programme 1", + "wbs": "0000/A0/01", + "invalid": false, + "from_date": "2019-01-01", + "to_date": "2019-12-31" + } + }, + { + "model": "users.user", + "pk": 6664, + "fields": { + "created": "2019-09-06T09:50:26.851Z", + "modified": "2019-09-06T09:50:26.862Z", + "username": "QvnLzWzvHUSg", + "email": "staff@unicef.org", + "password": "md5$EcJak3vWmN4B$97409c09f89ec7a732d1938241774a34", + "first_name": "", + "middle_name": "", + "last_name": "", + "date_joined": "2019-09-06T09:50:26.851Z", + "last_login": null, + "is_active": true, + "is_staff": true, + "is_superuser": false, + "groups": [ + 410 + ], + "user_permissions": [] + } + }, + { + "model": "auth.group", + "pk": 410, + "fields": { + "name": "UNICEF User", + "permissions": [] + } + }, + { + "model": "partners.partnerstaffmember", + "pk": 2015, + "fields": { + "created": "2019-09-06T09:50:26.812Z", + "modified": "2019-09-06T09:50:26.812Z", + "partner": 2015, + "title": "Jedi Master", + "first_name": "Mace", + "last_name": "Windu", + "email": "mace1@example.com", + "phone": "", + "active": true + } + }, + { + "model": "reports.result", + "pk": 1871, + "fields": { + "country_programme": null, + "result_type": 1871, + "sector": null, + "name": "Result 0", + "code": null, + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "parent": null, + "humanitarian_tag": false, + "humanitarian_marker_code": null, + "humanitarian_marker_name": null, + "wbs": null, + "vision_id": null, + "gic_code": null, + "gic_name": null, + "sic_code": null, + "sic_name": null, + "activity_focus_code": null, + "activity_focus_name": null, + "hidden": false, + "ram": false, + "created": "2019-09-06T09:50:26.876Z", + "modified": "2019-09-06T09:50:26.876Z", + "lft": 1, + "rght": 2, + "tree_id": 1, + "level": 0 + } + }, + { + "model": "reports.resulttype", + "pk": 1871, + "fields": { + "name": "Output" + } + }, + { + "model": "reports.disaggregation", + "pk": 70, + "fields": { + "created": "2019-09-06T09:50:26.912Z", + "modified": "2019-09-06T09:50:26.912Z", + "name": "A Disaggregation", + "active": false + } + }, + { + "model": "locations.location", + "pk": 1893, + "fields": { + "name": "A Location", + "gateway": 1893, + "latitude": null, + "longitude": null, + "p_code": "a-p-code", + "parent": null, + "geom": null, + "point": "SRID=4326;POINT (20 20)", + "is_active": true, + "created": "2019-09-06T09:50:26.885Z", + "modified": "2019-09-06T09:50:26.885Z", + "lft": 1, + "rght": 2, + "tree_id": 1, + "level": 0 + } + }, + { + "model": "locations.gatewaytype", + "pk": 1893, + "fields": { + "created": "2019-09-06T09:50:26.884Z", + "modified": "2019-09-06T09:50:26.884Z", + "name": "A Gateway", + "admin_level": 0 + } + } + ] + }, + "file_type_attachment": { + "master": { + "model": "unicef_attachments.filetype", + "pk": 7531, + "fields": { + "order": 2, + "name": "file_type_2", + "label": "", + "code": "partners_intervention_attachment" + } + }, + "deps": [] + }, + "file_type_prc": { + "master": { + "model": "unicef_attachments.filetype", + "pk": 7532, + "fields": { + "order": 3, + "name": "file_type_3", + "label": "", + "code": "partners_intervention_prc_review" + } + }, + "deps": [] + }, + "file_type_pd": { + "master": { + "model": "unicef_attachments.filetype", + "pk": 7533, + "fields": { + "order": 4, + "name": "file_type_4", + "label": "", + "code": "partners_intervention_signed_pd" + } + }, + "deps": [] + }, + "disaggregation": { + "master": { + "model": "reports.disaggregation", + "pk": 70, + "fields": { + "created": "2019-09-06T09:50:26.912Z", + "modified": "2019-09-06T09:50:26.912Z", + "name": "A Disaggregation", + "active": false + } + }, + "deps": [] + } +} \ No newline at end of file diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/get__api_prp_v1_interventions__{'workspace': 'ZZZ'}.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/get__api_prp_v1_interventions__{'workspace': 'ZZZ'}.response.json new file mode 100644 index 0000000000..9534dfffe1 --- /dev/null +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api_prp/TestAPIPRP/get__api_prp_v1_interventions__{'workspace': 'ZZZ'}.response.json @@ -0,0 +1,143 @@ +{ + "status_code": 200, + "headers": { + "content-type": [ + "Content-Type", + "application/json" + ], + "vary": [ + "Vary", + "Accept, Origin, Cookie" + ], + "allow": [ + "Allow", + "GET" + ], + "x-frame-options": [ + "X-Frame-Options", + "SAMEORIGIN" + ], + "content-length": [ + "Content-Length", + "1877" + ] + }, + "data": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 2045, + "title": "Active Intervention", + "document_type": "PD", + "business_area_code": "ZZZ", + "offices": [], + "number": "TST/PCA20191980/PD20192045", + "status": "active", + "partner_org": { + "short_name": "", + "street_address": null, + "last_assessment_date": null, + "partner_type": "", + "cso_type": null, + "total_ct_cp": null, + "total_ct_cy": null, + "address": null, + "city": null, + "postal_code": null, + "country": null, + "id": 2015, + "unicef_vendor_number": null, + "name": "Partner 2", + "alternate_name": null, + "rating": null, + "email": null, + "phone_number": null, + "basis_for_risk_rating": "", + "core_values_assessment_date": null, + "type_of_assessment": null + }, + "special_reports": [], + "sections": [], + "agreement": "TST/PCA20191980", + "unicef_focal_points": [], + "agreement_auth_officers": [], + "focal_points": [], + "start_date": "2019-09-05", + "end_date": "2019-12-05", + "cso_budget": "0.00", + "cso_budget_currency": "", + "unicef_budget": "0.00", + "unicef_budget_currency": "", + "reporting_requirements": [ + { + "id": 40, + "start_date": "2001-01-29", + "end_date": "2010-04-07", + "due_date": "2018-05-28", + "report_type": "('HR', 'Humanitarian Report')" + } + ], + "expected_results": [ + { + "id": 109, + "title": "Lower Result 1", + "result_link": 1693, + "cp_output": { + "id": 1871, + "title": "Result 0" + }, + "indicators": [ + { + "id": 108, + "title": "The Blueprint", + "blueprint_id": 109, + "cluster_indicator_id": null, + "means_of_verification": null, + "baseline": { + "d": 1, + "v": 0 + }, + "target": { + "d": 1, + "v": 0 + }, + "locations": [ + { + "id": 1893, + "name": "A Location", + "pcode": "a-p-code", + "location_type": "A Gateway", + "admin_level": 0 + } + ], + "disaggregation": [ + { + "id": 70, + "name": "A Disaggregation", + "disaggregation_values": [] + } + ], + "is_high_frequency": false, + "is_active": true, + "numerator_label": null, + "denominator_label": null, + "unit": "number", + "display_type": "number" + } + ] + } + ], + "update_date": "2019-09-06T09:50:26.874000Z", + "amendments": [], + "locations": [], + "unicef_budget_cash": "0.00", + "unicef_budget_supplies": "0.00", + "disbursement": null, + "disbursement_percent": null + } + ] + }, + "content_type": null +} \ No newline at end of file diff --git a/src/etools/applications/partners/tests/test_api_prp.py b/src/etools/applications/partners/tests/test_api_prp.py index 07fd59244a..849bf30bc3 100644 --- a/src/etools/applications/partners/tests/test_api_prp.py +++ b/src/etools/applications/partners/tests/test_api_prp.py @@ -9,15 +9,23 @@ from rest_framework.test import APIRequestFactory from unicef_locations.tests.factories import GatewayTypeFactory, LocationFactory +from etools.applications.attachments.tests.factories import AttachmentFileTypeFactory from etools.applications.core.tests.cases import BaseTenantTestCase from etools.applications.core.tests.mixins import WorkspaceRequiredAPITestMixIn -from etools.applications.partners.models import InterventionResultLink, PartnerOrganization +from etools.applications.funds.tests.factories import FundsReservationHeaderFactory +from etools.applications.partners.models import ( + Intervention, + InterventionBudget, + InterventionResultLink, + PartnerOrganization, +) from etools.applications.partners.permissions import READ_ONLY_API_GROUP_NAME -from etools.applications.partners.tests.factories import InterventionFactory +from etools.applications.partners.tests.factories import AgreementFactory, InterventionFactory, PartnerFactory from etools.applications.partners.tests.test_utils import setup_intervention_test_data -from etools.applications.reports.models import AppliedIndicator, IndicatorBlueprint, LowerResult -from etools.applications.reports.tests.factories import ResultFactory +from etools.applications.reports.models import AppliedIndicator, IndicatorBlueprint, LowerResult, ResultType +from etools.applications.reports.tests.factories import ReportingRequirementFactory, ResultFactory from etools.applications.users.tests.factories import GroupFactory, UserFactory +from etools.libraries.tests.api_checker import ApiCheckerMixin, AssertTimeStampedMixin class TestInterventionsAPI(WorkspaceRequiredAPITestMixIn, BaseTenantTestCase): @@ -73,6 +81,8 @@ def test_prp_api(self): del expected_intervention['agreement'] del expected_intervention['partner_org']['unicef_vendor_number'] del actual_intervention['partner_org']['unicef_vendor_number'] + del expected_intervention['reporting_requirements'] + del actual_intervention['reporting_requirements'] for j in range(len(expected_intervention['expected_results'])): del expected_intervention['expected_results'][j]['id'] @@ -183,3 +193,85 @@ def test_staff_has_access(self): """Ensure a staff user has access""" response = self.forced_auth_req('get', self.url, user=UserFactory(is_staff=True), data=self.query_param_data) self.assertEquals(response.status_code, status.HTTP_200_OK) + + +class TestAPIPRP(ApiCheckerMixin, AssertTimeStampedMixin, WorkspaceRequiredAPITestMixIn, BaseTenantTestCase): + + def assert_results(self, response, expected, path=''): + extepted_results = json.loads(json.dumps(expected['results'][0])) + response_results = json.loads(json.dumps(response['results'][0])) + assert sorted(response_results.keys()) == sorted(extepted_results.keys()) + + def get_fixtures(self): + today = datetime.date.today() + + partnership_user = UserFactory(is_staff=True, email='partner@unicef.org') + partnership_user.groups.add(GroupFactory()) + + partner = PartnerFactory(name='Partner 1', vendor_number="VP1") + partner1 = PartnerFactory(name='Partner 2') + agreement = AgreementFactory(partner=partner, signed_by_unicef_date=datetime.date.today()) + active_agreement = AgreementFactory( + partner=partner1, + status='active', + signed_by_unicef_date=datetime.date.today(), + signed_by_partner_date=datetime.date.today()) + unicef_staff = UserFactory(is_staff=True, email='staff@unicef.org') + result_type = ResultType.objects.get_or_create(name=ResultType.OUTPUT)[0] + intervention = InterventionFactory(agreement=agreement, title='Intervention 1') + active_intervention = InterventionFactory( + agreement=active_agreement, title='Active Intervention', + document_type=Intervention.PD, start=today - datetime.timedelta(days=1), + end=today + datetime.timedelta(days=90), + status='active', + signed_by_unicef_date=today - datetime.timedelta(days=1), + signed_by_partner_date=today - datetime.timedelta(days=1), + unicef_signatory=unicef_staff, + partner_authorized_officer_signatory=partner1.staff_members.all().first() + ) + result = ResultFactory(result_type=result_type) + result_link = InterventionResultLink.objects.create(intervention=active_intervention, cp_output=result) + indicator_blueprint = IndicatorBlueprint.objects.create(title='The Blueprint') + lower_result = LowerResult.objects.create(result_link=result_link, name='Lower Result 1') + applied_indicator = AppliedIndicator.objects.create(indicator=indicator_blueprint, lower_result=lower_result, ) + applied_indicator.locations.add(LocationFactory( + name='A Location', gateway=GatewayTypeFactory(name='A Gateway'), p_code='a-p-code')) + disaggregation = applied_indicator.disaggregation.create(name='A Disaggregation') + + return { + "unicef_staff": unicef_staff, + "partnership_manager_user": partnership_user, + "partner": partner, + "partner1": partner1, + "agreement": agreement, + "active_agreement": active_agreement, + "intervention": intervention, + "intervention_2": InterventionFactory(agreement=agreement, title='Intervention 2', + document_type=Intervention.PD), + "active_intervention": active_intervention, + "reporting_requirement": ReportingRequirementFactory(intervention=active_intervention), + "result_type": result_type, + "result": result, + "partnership_budget": InterventionBudget.objects.create( + intervention=intervention, + unicef_cash=10, + unicef_cash_local=100, + partner_contribution=20, + partner_contribution_local=200, + in_kind_amount_local=10, + ), + "fr_1": FundsReservationHeaderFactory(intervention=None, currency='USD'), + "fr_2": FundsReservationHeaderFactory(intervention=None, currency='USD'), + "result_link": result_link, + "lower_result": lower_result, + "indicator_blueprint": indicator_blueprint, + "applied_indicator": applied_indicator, + "file_type_attachment": AttachmentFileTypeFactory(code="partners_intervention_attachment"), + "file_type_prc": AttachmentFileTypeFactory(code="partners_intervention_prc_review"), + "file_type_pd": AttachmentFileTypeFactory(code="partners_intervention_signed_pd"), + "disaggregation": disaggregation + } + + def test_prp_api(self): + url = reverse('prp_api_v1:prp-intervention-list') + self.assertGET(url, data={'workspace': self.tenant.business_area_code}) diff --git a/src/etools/applications/partners/tests/test_utils.py b/src/etools/applications/partners/tests/test_utils.py index 2207591e70..73153d795c 100644 --- a/src/etools/applications/partners/tests/test_utils.py +++ b/src/etools/applications/partners/tests/test_utils.py @@ -14,7 +14,11 @@ from etools.applications.partners.models import Agreement, Intervention, InterventionBudget, InterventionResultLink from etools.applications.partners.tests.factories import AgreementFactory, InterventionFactory, PartnerFactory from etools.applications.reports.models import AppliedIndicator, IndicatorBlueprint, LowerResult, ResultType -from etools.applications.reports.tests.factories import CountryProgrammeFactory, ResultFactory +from etools.applications.reports.tests.factories import ( + CountryProgrammeFactory, + ReportingRequirementFactory, + ResultFactory, +) from etools.applications.users.tests.factories import GroupFactory, UserFactory @@ -52,7 +56,7 @@ def setup_intervention_test_data(test_case, include_results_and_indicators=False unicef_signatory=test_case.unicef_staff, partner_authorized_officer_signatory=test_case.partner1.staff_members.all().first() ) - + test_case.reporting_requirement = ReportingRequirementFactory(intervention=test_case.active_intervention) test_case.result_type = ResultType.objects.get_or_create(name=ResultType.OUTPUT)[0] test_case.result = ResultFactory(result_type=test_case.result_type) diff --git a/src/etools/applications/partners/views/prp_v1.py b/src/etools/applications/partners/views/prp_v1.py index f92574a06b..926d46f0aa 100644 --- a/src/etools/applications/partners/views/prp_v1.py +++ b/src/etools/applications/partners/views/prp_v1.py @@ -50,8 +50,8 @@ class PRPInterventionListAPIView(QueryStringFilterMixin, ListAPIView): pagination_class = PRPInterventionPagination queryset = Intervention.objects.filter( - result_links__ll_results__applied_indicators__id__isnull=False, - reporting_requirements__id__isnull=False, + result_links__ll_results__applied_indicators__isnull=False, + reporting_requirements__isnull=False, in_amendment=False ).prefetch_related( 'result_links__cp_output',