From ac522a0a192deef27d9e823a85c4405815342a75 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 28 Aug 2018 11:12:57 -0400 Subject: [PATCH 01/42] AD task improvements --- src/etools/applications/users/admin.py | 1 + src/etools/applications/users/tasks.py | 22 +++++++++++--- .../applications/users/tests/test_tasks.py | 30 ++++++++++++------- .../libraries/azure_graph_api/client.py | 8 ++--- src/etools/libraries/azure_graph_api/tasks.py | 13 +++++--- .../azure_graph_api/tests/test_client.py | 5 ++-- .../azure_graph_api/tests/test_utils.py | 3 +- src/etools/libraries/azure_graph_api/utils.py | 17 ++++++++--- 8 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index 4568b196da..9205a6aa4c 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -174,6 +174,7 @@ class UserAdminPlus(UserAdmin): 'last_name', 'office', 'is_staff', + 'is_superuser', 'is_active', ] diff --git a/src/etools/applications/users/tasks.py b/src/etools/applications/users/tasks.py index 680f4006a0..588a026e83 100644 --- a/src/etools/applications/users/tasks.py +++ b/src/etools/applications/users/tasks.py @@ -118,9 +118,11 @@ def _set_attribute(self, obj, attr, value): @transaction.atomic def create_or_update_user(self, record): - + status = {'processed': 0, 'created': 0, 'updated': 0, 'skipped': 0, 'errors': 0} + status['processed'] = 1 if not self.record_is_valid(record): - return + status['skipped'] = 1 + return status key_value = record[self.KEY_ATTRIBUTE] logger.debug(key_value) @@ -130,6 +132,7 @@ def create_or_update_user(self, record): email=key_value, username=key_value, is_staff=True) if created: + status['created'] = int(created) user.set_unusable_password() user.groups.add(self.groups['UNICEF User']) logger.info(u'Group added to user {}'.format(user)) @@ -140,11 +143,18 @@ def create_or_update_user(self, record): logger.warning(u'No profile for user {}'.format(user)) return - self.update_user(user, record) - self.update_profile(profile, record) + user_updated = self.update_user(user, record) + profile_updated = self.update_profile(profile, record) + + if not created and (user_updated or profile_updated): + status['updated'] = 1 except IntegrityError as e: logger.exception(u'Integrity error on user retrieving: {} - exception {}'.format(key_value, e)) + status['created'] = status['updated'] = 0 + status['errors'] = 1 + + return status def record_is_valid(self, record): if self.KEY_ATTRIBUTE not in record: @@ -178,6 +188,8 @@ def update_user(self, user, record): logger.debug(f'Updated User: {user}') user.save() + return modified + def update_profile(self, profile, record): modified = False for attr, record_attr in self.PROFILE_ATTR_MAP.items(): @@ -190,6 +202,8 @@ def update_profile(self, profile, record): logger.debug(f'Updated Profile: {profile.user}') profile.save() + return modified + class AzureUserMapper(UserMapper): KEY_ATTRIBUTE = 'userPrincipalName' diff --git a/src/etools/applications/users/tests/test_tasks.py b/src/etools/applications/users/tests/test_tasks.py index 8e831cdf84..83035db162 100644 --- a/src/etools/applications/users/tests/test_tasks.py +++ b/src/etools/applications/users/tests/test_tasks.py @@ -21,7 +21,7 @@ def setUpTestData(cls): cls.group = GroupFactory(name="UNICEF User") def setUp(self): - self.mapper = tasks.UserMapper() + self.mapper = tasks.AzureUserMapper() def test_init(self): self.assertEqual(self.mapper.countries, {}) @@ -114,20 +114,23 @@ def test_set_attribute_special_field(self): def test_create_or_update_user_missing_fields(self): """If missing field, then don't create user record'""" email = "tester@example.com" - res = self.mapper.create_or_update_user({"internetaddress": email}) - self.assertIsNone(res) + res = self.mapper.create_or_update_user({"userPrincipalName": email}) + self.assertEqual(res, {'processed': 1, 'created': 0, 'updated': 0, 'skipped': 1, 'errors': 0}) self.assertFalse(get_user_model().objects.filter(email=email).exists()) def test_create_or_update_user_created(self): """Ensure user is created and added to default group""" email = "tester@example.com" res = self.mapper.create_or_update_user({ + "userPrincipalName": email, "internetaddress": email, "givenName": "Tester", "mail": email, - "sn": "Last" + "surname": "Last", + "userType": "Internal", + "companyName": "UNICEF", }) - self.assertIsNone(res) + self.assertEqual(res, {'processed': 1, 'created': 1, 'updated': 0, 'skipped': 0, 'errors': 0}) self.assertTrue(get_user_model().objects.filter(email=email).exists()) self.assertTrue( UserProfile.objects.filter(user__email=email).exists() @@ -145,12 +148,16 @@ def test_create_or_update_user_exists(self): last_name="Last", ) res = self.mapper.create_or_update_user({ + "userPrincipalName": email, "internetaddress": email, "givenName": "Tester", "mail": email, - "sn": "Last" + "surname": "Last", + "userType": "Internal", + "companyName": "UNICEF", + }) - self.assertIsNone(res) + self.assertEqual(res, {'processed': 1, 'created': 0, 'updated': 0, 'skipped': 0, 'errors': 1}) user = get_user_model().objects.get(email=email) self.assertIn(self.group, user.groups.all()) @@ -160,12 +167,15 @@ def test_create_or_update_user_profile_updated(self): phone = "0987654321" res = self.mapper.create_or_update_user({ "internetaddress": email, + "userPrincipalName": email, "givenName": "Tester", "mail": email, - "sn": "Last", - "telephoneNumber": phone + "surname": "Last", + "userType": "Internal", + "companyName": "UNICEF", + "businessPhones": phone }) - self.assertIsNone(res) + self.assertEqual(res, {'processed': 1, 'created': 1, 'updated': 0, 'skipped': 0, 'errors': 0}) self.assertTrue(get_user_model().objects.filter(email=email).exists()) self.assertTrue( UserProfile.objects.filter(user__email=email).exists() diff --git a/src/etools/libraries/azure_graph_api/client.py b/src/etools/libraries/azure_graph_api/client.py index f8d03ae060..6f9c3ca0da 100644 --- a/src/etools/libraries/azure_graph_api/client.py +++ b/src/etools/libraries/azure_graph_api/client.py @@ -55,12 +55,12 @@ def analyse_page(url, access_token): jresponse = response.json() if response.status_code == 200: logger.info('Azure: Information retrieved') - handle_records(jresponse) + status = handle_records(jresponse) url = jresponse.get('@odata.nextLink', None) else: logger.error('Error during synchronization process') raise AzureHttpError('Error processing the response {}'.format(response.status_code), response.status_code) - return url, jresponse.get('@odata.deltaLink', None) + return url, status, jresponse.get('@odata.deltaLink', None) def azure_sync_users(url): @@ -72,5 +72,5 @@ def azure_sync_users(url): access_token = get_token() delta_link = None while url: - url, delta_link = analyse_page(url, access_token) - return delta_link + url, status, delta_link = analyse_page(url, access_token) + return status, delta_link diff --git a/src/etools/libraries/azure_graph_api/tasks.py b/src/etools/libraries/azure_graph_api/tasks.py index 5f6b63bdd2..955f1206e1 100644 --- a/src/etools/libraries/azure_graph_api/tasks.py +++ b/src/etools/libraries/azure_graph_api/tasks.py @@ -39,12 +39,14 @@ def sync_all_users(): settings.AZURE_GRAPH_API_VERSION, settings.AZURE_GRAPH_API_PAGE_SIZE ) - azure_sync_users(url) + status, _ = azure_sync_users(url) except Exception as e: log.exception_message = force_text(e) raise VisionException(*e.args) else: - log.successful = True + log.total_records = status['processed'] + status['skipped'] + log.total_processed = status['processed'] + log.successful = status['created'] + status['updated'] finally: log.save() logger.info('Azure Complete Sync Process finished') @@ -63,12 +65,15 @@ def sync_delta_users(): settings.AZURE_GRAPH_API_PAGE_SIZE ) ) - delta_link = azure_sync_users(url) + status, delta_link = azure_sync_users(url) cache.set(AZURE_GRAPH_API_USER_CACHE_KEY, delta_link) + except Exception as e: log.exception_message = force_text(e) raise VisionException(*e.args) else: + log.total_records = status['processed'] + status['skipped'] + log.total_processed = status['processed'] log.successful = True finally: log.save() @@ -91,5 +96,5 @@ def retrieve_user_info(username): jresponse = response.json() if response.status_code == 200: logger.info('Azure: Information retrieved') - return handle_record(jresponse) + return handle_record(jresponse)[-1] return {} diff --git a/src/etools/libraries/azure_graph_api/tests/test_client.py b/src/etools/libraries/azure_graph_api/tests/test_client.py index 31f942c243..05b28d3c51 100644 --- a/src/etools/libraries/azure_graph_api/tests/test_client.py +++ b/src/etools/libraries/azure_graph_api/tests/test_client.py @@ -31,7 +31,7 @@ def test_get_token_bad_request(self): @responses.activate @patch('etools.libraries.azure_graph_api.client.get_token', return_value='t0k3n') - @patch("etools.libraries.azure_graph_api.client.handle_records") + @patch("etools.libraries.azure_graph_api.client.handle_records", return_value='status') def test_azure_sync_users_ok(self, handle_function, token): url = '{}/{}/users?$top={}'.format( settings.AZURE_GRAPH_API_BASE_URL, @@ -42,7 +42,8 @@ def test_azure_sync_users_ok(self, handle_function, token): responses.GET, url, status=200, json={'@odata.deltaLink': 'delta'}, ) - delta = azure_sync_users(url) + status, delta = azure_sync_users(url) + self.assertEquals(status, 'status') self.assertEquals(delta, 'delta') self.assertEqual(token.call_count, 1) self.assertEqual(token.call_args[0], ()) diff --git a/src/etools/libraries/azure_graph_api/tests/test_utils.py b/src/etools/libraries/azure_graph_api/tests/test_utils.py index fa6a33f818..60ec313994 100644 --- a/src/etools/libraries/azure_graph_api/tests/test_utils.py +++ b/src/etools/libraries/azure_graph_api/tests/test_utils.py @@ -14,7 +14,8 @@ class TestClient(BaseTenantTestCase): def setUpTestData(cls): cls.group = GroupFactory(name='UNICEF User') - @patch('etools.libraries.azure_graph_api.utils.handle_record') + @patch('etools.libraries.azure_graph_api.utils.handle_record', return_value=( + {'processed': 0, 'created': 0, 'updated': 0, 'skipped': 0, 'errors': 0}, {})) def test_handle_records(self, handle_function): handle_records({'value': range(3)}) self.assertEqual(handle_function.call_count, 3) diff --git a/src/etools/libraries/azure_graph_api/utils.py b/src/etools/libraries/azure_graph_api/utils.py index f9d0fae638..35fe7aa1e7 100644 --- a/src/etools/libraries/azure_graph_api/utils.py +++ b/src/etools/libraries/azure_graph_api/utils.py @@ -7,17 +7,26 @@ def handle_records(jresponse): + if 'value' in jresponse: + status = {'processed': 0, 'created': 0, 'updated': 0, 'skipped': 0, 'errors': 0} for record in jresponse['value']: - handle_record(record) + page_status, _ = handle_record(record) + status['processed'] += page_status['processed'] + status['created'] += page_status['created'] + status['updated'] += page_status['updated'] + status['skipped'] += page_status['skipped'] + status['errors'] += page_status['errors'] else: - handle_record(jresponse) + status, _ = handle_record(jresponse) + + return status def handle_record(record): logger.debug('Azure: Information retrieved %s', record.get('userPrincipalName', '-')) user_sync = AzureUserMapper() - user_sync.create_or_update_user(record) + status = user_sync.create_or_update_user(record) record_dict = { 'ID': record.get('id', '-'), @@ -59,4 +68,4 @@ def handle_record(record): logger.debug(f'{label}: {value}') logger.debug('----------------------------------------') - return record_dict + return status, record_dict From 4ac1d97525413f3818fca368d5109c0c451bd077 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 5 Sep 2018 05:34:28 -0400 Subject: [PATCH 02/42] fixed intervention amendment admin --- src/etools/applications/partners/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index 5f5db8837a..3b91329b00 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -47,7 +47,7 @@ class InterventionAmendmentsAdmin(admin.ModelAdmin): 'types', 'signed_date' ) - search_fields = ('intervention', ) + search_fields = ('intervention__number', ) list_filter = ( 'intervention', 'types' From 7a6333dc0b82e1e82910507ed8d3286b6de30272 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 6 Sep 2018 11:47:22 -0400 Subject: [PATCH 03/42] partnership dashboard api filterable by pk --- src/etools/applications/partners/views/dashboards.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/applications/partners/views/dashboards.py b/src/etools/applications/partners/views/dashboards.py index a11e60630f..75b8ebedf2 100644 --- a/src/etools/applications/partners/views/dashboards.py +++ b/src/etools/applications/partners/views/dashboards.py @@ -25,6 +25,7 @@ class InterventionPartnershipDashView(QueryStringFilterMixin, ListCreateAPIView) search_param = 'qs' filters = ( + ('pk', 'pk__in'), ('status', 'status__in'), ('startAfter', 'start__gt'), ('startBefore', 'start__lt'), From 7823b204f901a0d04acea91c1f15df5eb556a560 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 7 Sep 2018 04:12:26 -0400 Subject: [PATCH 04/42] allow null on export serializers --- .../action_points/export/serializers.py | 22 +++++----- .../applications/tpm/export/serializers.py | 40 +++++++++---------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/etools/applications/action_points/export/serializers.py b/src/etools/applications/action_points/export/serializers.py index 0643f969ab..64900827b4 100644 --- a/src/etools/applications/action_points/export/serializers.py +++ b/src/etools/applications/action_points/export/serializers.py @@ -3,21 +3,21 @@ class ActionPointExportSerializer(serializers.Serializer): ref = serializers.CharField(source='reference_number', read_only=True) - cp_output = serializers.CharField(source='cp_output.__str__') - partner = serializers.CharField(source='partner.name') - office = serializers.CharField(source='office.name') - section = serializers.CharField(source='section.name') - category = serializers.CharField(source='category.description') - assigned_to = serializers.CharField(source='assigned_to.get_full_name') + cp_output = serializers.CharField(source='cp_output.__str__', allow_null=True) + partner = serializers.CharField(source='partner.name', allow_null=True) + office = serializers.CharField(source='office.name', allow_null=True) + section = serializers.CharField(source='section.name', allow_null=True) + category = serializers.CharField(source='category.description', allow_null=True) + assigned_to = serializers.CharField(source='assigned_to.get_full_name', allow_null=True) due_date = serializers.DateField(format='%d/%m/%Y') status = serializers.CharField(source='get_status_display') description = serializers.CharField() - intervention = serializers.CharField(source='intervention.reference_number', read_only=True) - pd_ssfa = serializers.CharField(source='intervention.title') - location = serializers.CharField(source='location.__str__') + intervention = serializers.CharField(source='intervention.reference_number', read_only=True, allow_null=True) + pd_ssfa = serializers.CharField(source='intervention.title', allow_null=True) + location = serializers.CharField(source='location.__str__', allow_null=True) related_module = serializers.CharField() - assigned_by = serializers.CharField(source='assigned_by.get_full_name') + assigned_by = serializers.CharField(source='assigned_by.get_full_name', allow_null=True) date_of_completion = serializers.DateTimeField(format='%d/%m/%Y') - related_ref = serializers.CharField(source='related_object.reference_number', read_only=True) + related_ref = serializers.CharField(source='related_object.reference_number', read_only=True, allow_null=True) related_object_str = serializers.CharField() related_object_url = serializers.CharField() diff --git a/src/etools/applications/tpm/export/serializers.py b/src/etools/applications/tpm/export/serializers.py index 7d759464cd..09a48e6f6a 100644 --- a/src/etools/applications/tpm/export/serializers.py +++ b/src/etools/applications/tpm/export/serializers.py @@ -63,12 +63,12 @@ def get_activity(self, obj): class TPMActionPointExportSerializer(serializers.Serializer): ref = serializers.CharField(source='reference_number') - assigned_to = serializers.CharField(source='assigned_to.get_full_name') + assigned_to = serializers.CharField(source='assigned_to.get_full_name', allow_null=True) author = serializers.CharField(source='author.get_full_name') section = serializers.CharField(source='tpm_activity.section') status = serializers.CharField(source='get_status_display') locations = serializers.SerializerMethodField() - cp_output = serializers.CharField(source='tpm_activity.cp_output') + cp_output = serializers.CharField(source='tpm_activity.cp_output', allow_null=True) due_date = serializers.DateField(format='%d/%m/%Y') description = serializers.CharField() @@ -85,18 +85,18 @@ class TPMVisitExportSerializer(serializers.Serializer): visit = serializers.CharField(source='*') status = serializers.CharField(source='get_status_display') activities = CommaSeparatedExportField(source='tpm_activities') - sections = CommaSeparatedExportField(source='tpm_activities.section') - partners = CommaSeparatedExportField(source='tpm_activities.partner') + sections = CommaSeparatedExportField(source='tpm_activities.section', allow_null=True) + partners = CommaSeparatedExportField(source='tpm_activities.partner', allow_null=True) interventions = CommaSeparatedExportField(source='tpm_activities.intervention', export_attr='reference_number') pd_ssfa = CommaSeparatedExportField(source='tpm_activities.intervention', export_attr='title') - locations = CommaSeparatedExportField(source='tpm_activities.locations') + locations = CommaSeparatedExportField(source='tpm_activities.locations', allow_null=True) start_date = serializers.CharField() end_date = serializers.CharField() unicef_focal_points = CommaSeparatedExportField() tpm_partner_focal_points = CommaSeparatedExportField() report_link = serializers.SerializerMethodField() attachments = serializers.SerializerMethodField() - additional_information = CommaSeparatedExportField(source='tpm_activities.additional_information') + additional_information = CommaSeparatedExportField(source='tpm_activities.additional_information', allow_null=True) link = serializers.CharField(source='get_object_url') def get_report_link(self, obj): @@ -128,17 +128,17 @@ class TPMPartnerExportSerializer(serializers.Serializer): class TPMPartnerContactsSerializer(serializers.Serializer): id = serializers.IntegerField() - email = serializers.CharField(source='user.email') - first_name = serializers.CharField(source='user.first_name') - last_name = serializers.CharField(source='user.last_name') - is_active = serializers.IntegerField(source='user.is_active') - job_title = serializers.CharField(source='user.profile.job_title') - phone_number = serializers.CharField(source='user.profile.phone_number') - org_id = serializers.CharField(source='tpm_partner.vendor_number') - org_name = serializers.CharField(source='tpm_partner.name') - org_email = serializers.CharField(source='tpm_partner.email') - org_phone = serializers.CharField(source='tpm_partner.phone_number') - org_country = serializers.CharField(source='tpm_partner.country') - org_city = serializers.CharField(source='tpm_partner.city') - org_address = serializers.CharField(source='tpm_partner.street_address') - org_postal_code = serializers.CharField(source='tpm_partner.postal_code') + email = serializers.CharField(source='user.email', allow_null=True) + first_name = serializers.CharField(source='user.first_name', allow_null=True) + last_name = serializers.CharField(source='user.last_name', allow_null=True) + is_active = serializers.IntegerField(source='user.is_active', allow_null=True) + job_title = serializers.CharField(source='user.profile.job_title', allow_null=True) + phone_number = serializers.CharField(source='user.profile.phone_number', allow_null=True) + org_id = serializers.CharField(source='tpm_partner.vendor_number', allow_null=True) + org_name = serializers.CharField(source='tpm_partner.name', allow_null=True) + org_email = serializers.CharField(source='tpm_partner.email', allow_null=True) + org_phone = serializers.CharField(source='tpm_partner.phone_number', allow_null=True) + org_country = serializers.CharField(source='tpm_partner.country', allow_null=True) + org_city = serializers.CharField(source='tpm_partner.city', allow_null=True) + org_address = serializers.CharField(source='tpm_partner.street_address', allow_null=True) + org_postal_code = serializers.CharField(source='tpm_partner.postal_code', allow_null=True) From e65ba5f54d1927b0e2ceba8e2b6afa3627326f80 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Mon, 10 Sep 2018 11:29:17 +0300 Subject: [PATCH 05/42] added default values to serializers where attribute error can appear --- .../action_points/export/serializers.py | 18 +++++++++--------- .../applications/audit/serializers/auditor.py | 2 +- .../audit/serializers/engagement.py | 8 ++++---- .../applications/audit/serializers/export.py | 12 ++++++------ .../applications/tpm/export/serializers.py | 8 ++++---- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/etools/applications/action_points/export/serializers.py b/src/etools/applications/action_points/export/serializers.py index 0643f969ab..64e44f5984 100644 --- a/src/etools/applications/action_points/export/serializers.py +++ b/src/etools/applications/action_points/export/serializers.py @@ -3,21 +3,21 @@ class ActionPointExportSerializer(serializers.Serializer): ref = serializers.CharField(source='reference_number', read_only=True) - cp_output = serializers.CharField(source='cp_output.__str__') - partner = serializers.CharField(source='partner.name') - office = serializers.CharField(source='office.name') - section = serializers.CharField(source='section.name') - category = serializers.CharField(source='category.description') + cp_output = serializers.CharField(source='cp_output') + partner = serializers.CharField(source='partner.name', default='') + office = serializers.CharField(source='office.name', default='') + section = serializers.CharField(source='section.name', default='') + category = serializers.CharField(source='category.description', default='') assigned_to = serializers.CharField(source='assigned_to.get_full_name') due_date = serializers.DateField(format='%d/%m/%Y') status = serializers.CharField(source='get_status_display') description = serializers.CharField() - intervention = serializers.CharField(source='intervention.reference_number', read_only=True) - pd_ssfa = serializers.CharField(source='intervention.title') - location = serializers.CharField(source='location.__str__') + intervention = serializers.CharField(source='intervention.reference_number', read_only=True, default='') + pd_ssfa = serializers.CharField(source='intervention.title', default='') + location = serializers.CharField(source='location') related_module = serializers.CharField() assigned_by = serializers.CharField(source='assigned_by.get_full_name') date_of_completion = serializers.DateTimeField(format='%d/%m/%Y') - related_ref = serializers.CharField(source='related_object.reference_number', read_only=True) + related_ref = serializers.CharField(source='related_object.reference_number', read_only=True, default='') related_object_str = serializers.CharField() related_object_url = serializers.CharField() diff --git a/src/etools/applications/audit/serializers/auditor.py b/src/etools/applications/audit/serializers/auditor.py index d5278983a8..3aa8d57fde 100644 --- a/src/etools/applications/audit/serializers/auditor.py +++ b/src/etools/applications/audit/serializers/auditor.py @@ -114,7 +114,7 @@ class Meta(WritableNestedSerializerMixin.Meta): class AuditUserSerializer(UserSerializer): auditor_firm = serializers.SerializerMethodField() hidden = serializers.SerializerMethodField() - staff_member_id = serializers.ReadOnlyField(source='purchase_order_auditorstaffmember.id') + staff_member_id = serializers.ReadOnlyField(source='purchase_order_auditorstaffmember_id') class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + ['id', 'auditor_firm', 'hidden', 'staff_member_id', ] diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index fb24a14a15..1113af84a0 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -136,11 +136,11 @@ def create(self, validated_data): class EngagementExportSerializer(serializers.ModelSerializer): - agreement_number = serializers.ReadOnlyField(source='agreement.order_number') + agreement_number = serializers.ReadOnlyField(source='agreement.order_number', default='') engagement_type = serializers.ReadOnlyField(source='get_engagement_type_display') - partner_name = serializers.ReadOnlyField(source='partner.name') - auditor_firm_vendor_number = serializers.ReadOnlyField(source='agreement.auditor_firm.vendor_number') - auditor_firm_name = serializers.ReadOnlyField(source='agreement.auditor_firm.name') + partner_name = serializers.ReadOnlyField(source='partner.name', default='') + auditor_firm_vendor_number = serializers.ReadOnlyField(source='agreement.auditor_firm.vendor_number', default='') + auditor_firm_name = serializers.ReadOnlyField(source='agreement.auditor_firm.name', default='') status = serializers.ChoiceField( choices=Engagement.DISPLAY_STATUSES, source='displayed_status', diff --git a/src/etools/applications/audit/serializers/export.py b/src/etools/applications/audit/serializers/export.py index 5fadb999f4..faf3e0c824 100644 --- a/src/etools/applications/audit/serializers/export.py +++ b/src/etools/applications/audit/serializers/export.py @@ -69,8 +69,8 @@ def get_address(self, obj): class StaffMemberPDFSerializer(serializers.ModelSerializer): first_name = serializers.CharField(source='user.first_name') last_name = serializers.CharField(source='user.last_name') - job_title = serializers.CharField(source='user.profile.job_title') - phone_number = serializers.CharField(source='user.profile.phone_number') + job_title = serializers.CharField(source='user.profile.job_title', default='') + phone_number = serializers.CharField(source='user.profile.phone_number', default='') email = serializers.CharField(source='user.email') class Meta: @@ -84,8 +84,8 @@ class EngagementActionPointPDFSerializer(serializers.ModelSerializer): status = serializers.CharField(source='get_status_display') due_date = serializers.DateField(format='%d %b %Y') assigned_to = serializers.CharField(source='assigned_to.get_full_name') - office = serializers.CharField(source='office.name') - section = serializers.CharField(source='section.name') + office = serializers.CharField(source='office.name', default='') + section = serializers.CharField(source='section.name', default='') class Meta: model = EngagementActionPoint @@ -245,8 +245,8 @@ class Meta(EngagementPDFSerializer.Meta): class EngagementBaseDetailCSVSerializer(serializers.Serializer): unique_id = serializers.ReadOnlyField() link = serializers.ReadOnlyField(source='get_object_url') - auditor = serializers.ReadOnlyField(source='agreement.auditor_firm.__str__') - partner = serializers.ReadOnlyField(source='partner.__str__') + auditor = serializers.ReadOnlyField(source='agreement.auditor_firm') + partner = serializers.ReadOnlyField(source='partner') status_display = serializers.SerializerMethodField() def get_status_display(self, obj): diff --git a/src/etools/applications/tpm/export/serializers.py b/src/etools/applications/tpm/export/serializers.py index 7d759464cd..850a4edc3c 100644 --- a/src/etools/applications/tpm/export/serializers.py +++ b/src/etools/applications/tpm/export/serializers.py @@ -25,8 +25,8 @@ class TPMActivityExportSerializer(serializers.Serializer): section = serializers.CharField() cp_output = serializers.CharField() partner = serializers.CharField() - intervention = serializers.CharField(source='intervention.reference_number') - pd_ssfa = serializers.CharField(source='intervention.title') + intervention = serializers.CharField(source='intervention.reference_number', default='') + pd_ssfa = serializers.CharField(source='intervention.title', default='') locations = CommaSeparatedExportField() date = serializers.DateField(format='%d/%m/%Y') unicef_focal_points = UsersExportField() @@ -47,8 +47,8 @@ class TPMLocationExportSerializer(serializers.Serializer): section = serializers.CharField(source='activity.tpmactivity.section') cp_output = serializers.CharField(source='activity.tpmactivity.cp_output') partner = serializers.CharField(source='activity.tpmactivity.partner') - intervention = serializers.CharField(source='activity.tpmactivity.intervention.reference_number') - pd_ssfa = serializers.CharField(source='activity.tpmactivity.intervention.title') + intervention = serializers.CharField(source='activity.tpmactivity.intervention.reference_number', default='') + pd_ssfa = serializers.CharField(source='activity.tpmactivity.intervention.title', default='') location = serializers.CharField() date = serializers.DateField(source='activity.tpmactivity.date', format='%d/%m/%Y') unicef_focal_points = UsersExportField(source='activity.tpmactivity.unicef_focal_points') From 7f86932415940eb561831c15d40fb27e4ee94501 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Mon, 10 Sep 2018 11:37:38 +0300 Subject: [PATCH 06/42] no need to use same source as field name --- src/etools/applications/audit/serializers/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/audit/serializers/export.py b/src/etools/applications/audit/serializers/export.py index faf3e0c824..dc63e95357 100644 --- a/src/etools/applications/audit/serializers/export.py +++ b/src/etools/applications/audit/serializers/export.py @@ -246,7 +246,7 @@ class EngagementBaseDetailCSVSerializer(serializers.Serializer): unique_id = serializers.ReadOnlyField() link = serializers.ReadOnlyField(source='get_object_url') auditor = serializers.ReadOnlyField(source='agreement.auditor_firm') - partner = serializers.ReadOnlyField(source='partner') + partner = serializers.ReadOnlyField() status_display = serializers.SerializerMethodField() def get_status_display(self, obj): From 10bca98f9d7eae35d9eb243cfad96c3f4f27ba4f Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Mon, 10 Sep 2018 11:57:21 +0300 Subject: [PATCH 07/42] fixed staff spot checks export filename --- src/etools/applications/audit/views.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index 0bf8fabc3b..b8a5779bbd 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -388,7 +388,12 @@ class EngagementManagementMixin( PermittedFSMActionMixin, ): def get_export_filename(self, format=None): - return '{}.{}'.format(self.get_object().unique_id, (format or '').lower()) + instance = self.get_object() + + if instance: + return '{}.{}'.format(instance.unique_id, (format or '').lower()) + + return super().get_export_filename(format=format) class MicroAssessmentViewSet(EngagementManagementMixin, EngagementViewSet): @@ -396,6 +401,7 @@ class MicroAssessmentViewSet(EngagementManagementMixin, EngagementViewSet): serializer_class = MicroAssessmentSerializer export_serializer_class = MicroAssessmentDetailCSVSerializer renderer_classes = [JSONRenderer, MicroAssessmentDetailCSVRenderer] + export_filename = 'microassessments' class AuditViewSet(EngagementManagementMixin, EngagementViewSet): @@ -403,6 +409,7 @@ class AuditViewSet(EngagementManagementMixin, EngagementViewSet): serializer_class = AuditSerializer export_serializer_class = AuditDetailCSVSerializer renderer_classes = [JSONRenderer, AuditDetailCSVRenderer] + export_filename = 'audits' class SpotCheckViewSet(EngagementManagementMixin, EngagementViewSet): @@ -410,6 +417,7 @@ class SpotCheckViewSet(EngagementManagementMixin, EngagementViewSet): serializer_class = SpotCheckSerializer export_serializer_class = SpotCheckDetailCSVSerializer renderer_classes = [JSONRenderer, SpotCheckDetailCSVRenderer] + export_filename = 'spot-checks' class StaffSpotCheckViewSet(SpotCheckViewSet): @@ -418,6 +426,7 @@ class StaffSpotCheckViewSet(SpotCheckViewSet): serializer_action_classes = { 'list': StaffSpotCheckListSerializer } + export_filename = 'staff-spot-checks' class SpecialAuditViewSet(EngagementManagementMixin, EngagementViewSet): From 6e399bbb321f0a63bb70fd509a25e873371fc802 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 6 Sep 2018 13:11:15 -0400 Subject: [PATCH 08/42] include frs info --- src/etools/applications/funds/admin.py | 2 + src/etools/applications/partners/models.py | 3 - .../partners/serializers/interventions_v2.py | 4 +- .../TestAPIAgreements/fixtures.json | 60 ++-- .../get__api_v2_agreements_.response.json | 14 +- ...> get__api_v2_agreements_5_.response.json} | 140 +++++----- ...pi_v2_agreements_amendments_.response.json | 8 +- .../TestAPIIntervention/fixtures.json | 154 +++++----- .../get__api_v2_interventions_.response.json | 12 +- ...t__api_v2_interventions_101_.response.json | 264 +++++++++--------- ...v2_interventions_amendments_.response.json | 18 +- ...v2_interventions_indicators_.response.json | 2 +- ...t__api_v2_interventions_map_.response.json | 44 ++- .../TestPartners/fixtures.json | 10 +- .../get__api_v2_partners_.response.json | 2 +- .../get__api_v2_partners_101_.response.json | 4 +- ...ners_not_programmatic_visit_.response.json | 2 +- 17 files changed, 369 insertions(+), 374 deletions(-) rename src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/{get__api_v2_agreements_10593_.response.json => get__api_v2_agreements_5_.response.json} (84%) diff --git a/src/etools/applications/funds/admin.py b/src/etools/applications/funds/admin.py index 750f15ff14..a2ee585b95 100644 --- a/src/etools/applications/funds/admin.py +++ b/src/etools/applications/funds/admin.py @@ -11,10 +11,12 @@ class GrantAdmin(admin.ModelAdmin): class FRAdmin(admin.ModelAdmin): search_fields = ('fr_number',) + list_display = ('fr_number', 'vendor_code') class FRAdminLi(admin.ModelAdmin): search_fields = ('fr_ref_number',) + list_display = ('fr_ref_number', 'donor', 'donor_code', 'grant_number') admin.site.register(Grant, GrantAdmin) diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index e4dad5d4b9..1a05e25bc6 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -1518,9 +1518,6 @@ def frs_qs(self): def maps_qs(self): qs = self.get_queryset().prefetch_related('flat_locations').distinct().annotate( - donors=StringConcat("frs__fr_items__donor", separator="|", distinct=True), - donor_codes=StringConcat("frs__fr_items__donor_code", separator="|", distinct=True), - grants=StringConcat("frs__fr_items__grant_number", separator="|", distinct=True), results=StringConcat("result_links__cp_output__name", separator="|", distinct=True), clusters=StringConcat("result_links__ll_results__applied_indicators__cluster_indicator_title", separator="|", distinct=True), diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 6d5893b456..8ce78d34ac 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -13,7 +13,7 @@ from unicef_snapshot.serializers import SnapshotModelSerializer from etools.applications.funds.models import FundsCommitmentItem, FundsReservationHeader -from etools.applications.funds.serializers import FRsSerializer +from etools.applications.funds.serializers import FRsSerializer, FRHeaderSerializer from etools.applications.partners.models import ( Intervention, InterventionAmendment, @@ -656,6 +656,7 @@ class InterventionListMapSerializer(serializers.ModelSerializer): grants = serializers.ReadOnlyField(read_only=True) results = serializers.ReadOnlyField(source='cp_output_names', read_only=True) clusters = serializers.ReadOnlyField(read_only=True) + frs = FRHeaderSerializer(many=True, read_only=True) class Meta: model = Intervention @@ -679,6 +680,7 @@ class Meta: "grants", "results", "clusters", + "frs", ) diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/fixtures.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/fixtures.json index 3c1aca4ec1..afaff3ec38 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/fixtures.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/fixtures.json @@ -2,22 +2,22 @@ "agreement": { "master": { "model": "partners.agreement", - "pk": 10593, + "pk": 5, "fields": { - "created": "2018-08-28T19:00:48.864Z", - "modified": "2018-08-28T19:00:48.879Z", - "partner": 9893, - "country_programme": 9647, + "created": "2018-09-10T10:17:05.205Z", + "modified": "2018-09-10T10:17:05.231Z", + "partner": 5, + "country_programme": 5, "agreement_type": "PCA", - "agreement_number": "TST/PCA201810593", + "agreement_number": "TST/PCA20185", "attached_agreement": "", - "start": "2018-08-28", + "start": "2018-09-10", "end": "2018-12-31", "reference_number_year": 2018, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-08-28", + "signed_by_unicef_date": "2018-09-10", "signed_by": null, - "signed_by_partner_date": "2018-08-28", + "signed_by_partner_date": "2018-09-10", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -26,10 +26,10 @@ "deps": [ { "model": "partners.partnerorganization", - "pk": 9893, + "pk": 5, "fields": { - "created": "2018-08-28T19:00:48.857Z", - "modified": "2018-08-28T19:00:48.861Z", + "created": "2018-09-10T10:17:05.193Z", + "modified": "2018-09-10T10:17:05.199Z", "partner_type": "", "cso_type": null, "name": "Partner 4", @@ -106,7 +106,7 @@ }, { "model": "reports.countryprogramme", - "pk": 9647, + "pk": 5, "fields": { "name": "Country Programme 4", "wbs": "0000/A0/04", @@ -120,12 +120,12 @@ "agreement_amendment": { "master": { "model": "partners.agreementamendment", - "pk": 373, + "pk": 1, "fields": { - "created": "2018-08-28T19:00:48.880Z", - "modified": "2018-08-28T19:00:48.882Z", + "created": "2018-09-10T10:17:05.234Z", + "modified": "2018-09-10T10:17:05.267Z", "number": "tmp01", - "agreement": 10593, + "agreement": 5, "signed_amendment": "", "types": "[\"Change in clause\"]", "signed_date": null @@ -134,22 +134,22 @@ "deps": [ { "model": "partners.agreement", - "pk": 10593, + "pk": 5, "fields": { - "created": "2018-08-28T19:00:48.864Z", - "modified": "2018-08-28T19:00:48.879Z", - "partner": 9893, - "country_programme": 9647, + "created": "2018-09-10T10:17:05.205Z", + "modified": "2018-09-10T10:17:05.231Z", + "partner": 5, + "country_programme": 5, "agreement_type": "PCA", - "agreement_number": "TST/PCA201810593", + "agreement_number": "TST/PCA20185", "attached_agreement": "", - "start": "2018-08-28", + "start": "2018-09-10", "end": "2018-12-31", "reference_number_year": 2018, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-08-28", + "signed_by_unicef_date": "2018-09-10", "signed_by": null, - "signed_by_partner_date": "2018-08-28", + "signed_by_partner_date": "2018-09-10", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -157,10 +157,10 @@ }, { "model": "partners.partnerorganization", - "pk": 9893, + "pk": 5, "fields": { - "created": "2018-08-28T19:00:48.857Z", - "modified": "2018-08-28T19:00:48.861Z", + "created": "2018-09-10T10:17:05.193Z", + "modified": "2018-09-10T10:17:05.199Z", "partner_type": "", "cso_type": null, "name": "Partner 4", @@ -237,7 +237,7 @@ }, { "model": "reports.countryprogramme", - "pk": 9647, + "pk": 5, "fields": { "name": "Country Programme 4", "wbs": "0000/A0/04", diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_.response.json index 31006570d1..565ea56db8 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_.response.json @@ -20,16 +20,16 @@ }, "data": [ { - "id": 10593, - "partner": 9893, - "country_programme": 9647, - "agreement_number": "TST/PCA201810593", + "id": 5, + "partner": 5, + "country_programme": 5, + "agreement_number": "TST/PCA20185", "partner_name": "Partner 4", "agreement_type": "PCA", "end": "2018-12-31", - "start": "2018-08-28", - "signed_by_unicef_date": "2018-08-28", - "signed_by_partner_date": "2018-08-28", + "start": "2018-09-10", + "signed_by_unicef_date": "2018-09-10", + "signed_by_partner_date": "2018-09-10", "status": "signed" } ], diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_10593_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_5_.response.json similarity index 84% rename from src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_10593_.response.json rename to src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_5_.response.json index ab8ed61a8f..85e1f9e131 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_10593_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_5_.response.json @@ -19,15 +19,15 @@ ] }, "data": { - "id": 10593, + "id": 5, "partner_name": "Partner 4", "authorized_officers": [], "amendments": [ { - "id": 373, + "id": 1, "number": "tmp01", - "created": "2018-08-28T19:00:48.880000Z", - "modified": "2018-08-28T19:00:48.882000Z", + "created": "2018-09-10T10:17:05.234000Z", + "modified": "2018-09-10T10:17:05.267000Z", "signed_amendment_file": null, "signed_amendment_attachment": null, "signed_amendment": null, @@ -35,7 +35,7 @@ "Change in clause" ], "signed_date": null, - "agreement": 10593 + "agreement": 5 } ], "unicef_signatory": null, @@ -44,101 +44,101 @@ "attachment": null, "permissions": { "edit": { - "modified": true, - "partner_manager": false, - "interventions": true, - "reference_number_year": false, - "end": false, - "signed_by_partner_date": false, - "signed_by": false, + "country_programme_id": true, "special_conditions_pca": false, + "interventions": true, + "status": true, + "amendments": false, "country_programme": false, - "partner_id": true, - "created": true, + "partner_manager": false, + "reference_number_year": false, "partner": false, - "signed_by_id": true, - "agreement_type": false, + "partner_id": true, "start": false, - "attached_agreement": false, - "authorized_officers": false, - "amendments": false, + "partner_manager_id": true, "attachment": false, - "signed_by_unicef_date": false, - "id": true, - "status": true, - "country_programme_id": true, + "signed_by_partner_date": false, + "created": true, "agreement_number": false, - "partner_manager_id": true - }, - "required": { - "modified": false, - "partner_manager": false, - "interventions": false, - "reference_number_year": true, + "signed_by_unicef_date": false, "end": false, - "signed_by_partner_date": false, + "authorized_officers": false, + "signed_by_id": true, + "attached_agreement": false, + "modified": true, "signed_by": false, + "agreement_type": false, + "id": true + }, + "required": { + "country_programme_id": false, "special_conditions_pca": false, + "interventions": false, + "status": false, + "amendments": false, "country_programme": false, - "partner_id": false, - "created": false, + "partner_manager": false, + "reference_number_year": true, "partner": true, - "signed_by_id": false, - "agreement_type": true, + "partner_id": false, "start": false, - "attached_agreement": false, - "authorized_officers": false, - "amendments": false, + "partner_manager_id": false, "attachment": false, - "signed_by_unicef_date": false, - "id": false, - "status": false, - "country_programme_id": false, + "signed_by_partner_date": false, + "created": false, "agreement_number": true, - "partner_manager_id": false - }, - "view": { - "modified": true, - "partner_manager": false, - "interventions": true, - "reference_number_year": true, + "signed_by_unicef_date": false, "end": false, - "signed_by_partner_date": false, + "authorized_officers": false, + "signed_by_id": false, + "attached_agreement": false, + "modified": false, "signed_by": false, + "agreement_type": true, + "id": false + }, + "view": { + "country_programme_id": true, "special_conditions_pca": true, + "interventions": true, + "status": true, + "amendments": false, "country_programme": false, - "partner_id": true, - "created": true, + "partner_manager": false, + "reference_number_year": true, "partner": false, - "signed_by_id": true, - "agreement_type": false, + "partner_id": true, "start": false, - "attached_agreement": false, - "authorized_officers": false, - "amendments": false, + "partner_manager_id": true, "attachment": false, - "signed_by_unicef_date": false, - "id": true, - "status": true, - "country_programme_id": true, + "signed_by_partner_date": false, + "created": true, "agreement_number": false, - "partner_manager_id": true + "signed_by_unicef_date": false, + "end": false, + "authorized_officers": false, + "signed_by_id": true, + "attached_agreement": false, + "modified": true, + "signed_by": false, + "agreement_type": false, + "id": true } }, - "created": "2018-08-28T19:00:48.864000Z", - "modified": "2018-08-28T19:00:48.879000Z", + "created": "2018-09-10T10:17:05.205000Z", + "modified": "2018-09-10T10:17:05.231000Z", "agreement_type": "PCA", - "agreement_number": "TST/PCA201810593", + "agreement_number": "TST/PCA20185", "attached_agreement": null, - "start": "2018-08-28", + "start": "2018-09-10", "end": "2018-12-31", "reference_number_year": 2018, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-08-28", - "signed_by_partner_date": "2018-08-28", + "signed_by_unicef_date": "2018-09-10", + "signed_by_partner_date": "2018-09-10", "status": "signed", - "partner": 9893, - "country_programme": 9647, + "partner": 5, + "country_programme": 5, "signed_by": null, "partner_manager": null }, diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_amendments_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_amendments_.response.json index 1eea129ffd..16e4cf6b16 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_amendments_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestAPIAgreements/get__api_v2_agreements_amendments_.response.json @@ -20,16 +20,16 @@ }, "data": [ { - "id": 373, - "created": "2018-08-28T19:00:48.880653Z", - "modified": "2018-08-28T19:00:48.882599Z", + "id": 1, + "created": "2018-09-10T10:17:05.234037Z", + "modified": "2018-09-10T10:17:05.267219Z", "number": "tmp01", "signed_amendment": null, "types": [ "Change in clause" ], "signed_date": null, - "agreement": 10593 + "agreement": 5 } ], "content_type": null 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 92086d8c5c..25a44eac0d 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,17 +4,17 @@ "model": "partners.intervention", "pk": 101, "fields": { - "created": "2018-08-23T15:20:15.353Z", - "modified": "2018-08-23T15:20:15.356Z", + "created": "2018-09-10T10:17:06.137Z", + "modified": "2018-09-10T10:17:06.140Z", "document_type": "", - "agreement": 16001, + "agreement": 6, "country_programme": null, - "number": "TST/PCA201816001/2018101", + "number": "TST/PCA20186/2018101", "title": "Intervention Title 1", "status": "draft", "start": null, "end": null, - "submission_date": "2018-08-23", + "submission_date": "2018-09-10", "submission_date_prc": null, "reference_number_year": 2018, "review_date_prc": null, @@ -40,22 +40,22 @@ "deps": [ { "model": "partners.agreement", - "pk": 16001, + "pk": 6, "fields": { - "created": "2018-08-23T15:20:15.348Z", - "modified": "2018-08-23T15:20:15.352Z", - "partner": 15301, - "country_programme": 14933, + "created": "2018-09-10T10:17:06.118Z", + "modified": "2018-09-10T10:17:06.136Z", + "partner": 6, + "country_programme": 6, "agreement_type": "PCA", - "agreement_number": "TST/PCA201816001", - "attached_agreement": "test/file_attachments/partner_organization/15301/agreements/test_file.pdf", - "start": "2018-08-23", + "agreement_number": "TST/PCA20186", + "attached_agreement": "", + "start": "2018-09-10", "end": "2018-12-31", "reference_number_year": 2018, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-08-23", + "signed_by_unicef_date": "2018-09-10", "signed_by": null, - "signed_by_partner_date": "2018-08-23", + "signed_by_partner_date": "2018-09-10", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -63,10 +63,10 @@ }, { "model": "partners.partnerorganization", - "pk": 15301, + "pk": 6, "fields": { - "created": "2018-08-23T15:20:15.339Z", - "modified": "2018-08-23T15:20:15.345Z", + "created": "2018-09-10T10:17:06.109Z", + "modified": "2018-09-10T10:17:06.114Z", "partner_type": "", "cso_type": null, "name": "Partner 5", @@ -143,7 +143,7 @@ }, { "model": "reports.countryprogramme", - "pk": 14933, + "pk": 6, "fields": { "name": "Country Programme 5", "wbs": "0000/A0/05", @@ -157,34 +157,34 @@ "amendment": { "master": { "model": "partners.interventionamendment", - "pk": 674, + "pk": 1, "fields": { - "created": "2018-08-23T15:20:15.369Z", - "modified": "2018-08-23T15:20:15.388Z", - "intervention": 13915, - "types": "[\"Change in clause\"]", - "other_description": "nsVZMcJfJpIZWElxzhLSmnvQaXJLijVgcfvqdQMBcBXQHtGQIR", - "signed_date": "2018-08-23", + "created": "2018-09-10T10:17:06.159Z", + "modified": "2018-09-10T10:17:06.176Z", + "intervention": 3, + "types": "[\"Change banking info\"]", + "other_description": "GCbYeGGQeTyXjnWsiAJoQtAFpPVsIdPQecJebpiCTsDumQgMCy", + "signed_date": "2018-09-10", "amendment_number": 1, - "signed_amendment": "test/file_attachments/partner_organization/15302/15302/agreements/16002/interventions/13915/amendments/None/test_file.pdf" + "signed_amendment": "test/file_attachments/partner_organization/7/7/agreements/7/interventions/3/amendments/None/test_file.pdf" } }, "deps": [ { "model": "partners.intervention", - "pk": 13915, + "pk": 3, "fields": { - "created": "2018-08-23T15:20:15.366Z", - "modified": "2018-08-23T15:20:15.387Z", + "created": "2018-09-10T10:17:06.157Z", + "modified": "2018-09-10T10:17:06.174Z", "document_type": "", - "agreement": 16002, + "agreement": 7, "country_programme": null, - "number": "TST/PCA201816002/201813915", + "number": "TST/PCA20187/20183", "title": "Intervention Title 2", "status": "draft", "start": null, "end": null, - "submission_date": "2018-08-23", + "submission_date": "2018-09-10", "submission_date_prc": null, "reference_number_year": 2018, "review_date_prc": null, @@ -209,22 +209,22 @@ }, { "model": "partners.agreement", - "pk": 16002, + "pk": 7, "fields": { - "created": "2018-08-23T15:20:15.363Z", - "modified": "2018-08-23T15:20:15.365Z", - "partner": 15302, - "country_programme": 14934, + "created": "2018-09-10T10:17:06.147Z", + "modified": "2018-09-10T10:17:06.156Z", + "partner": 7, + "country_programme": 7, "agreement_type": "PCA", - "agreement_number": "TST/PCA201816002", - "attached_agreement": "test/file_attachments/partner_organization/15302/agreements/test_file.pdf", - "start": "2018-08-23", + "agreement_number": "TST/PCA20187", + "attached_agreement": "", + "start": "2018-09-10", "end": "2018-12-31", "reference_number_year": 2018, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-08-23", + "signed_by_unicef_date": "2018-09-10", "signed_by": null, - "signed_by_partner_date": "2018-08-23", + "signed_by_partner_date": "2018-09-10", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -232,10 +232,10 @@ }, { "model": "partners.partnerorganization", - "pk": 15302, + "pk": 7, "fields": { - "created": "2018-08-23T15:20:15.358Z", - "modified": "2018-08-23T15:20:15.361Z", + "created": "2018-09-10T10:17:06.142Z", + "modified": "2018-09-10T10:17:06.145Z", "partner_type": "", "cso_type": null, "name": "Partner 6", @@ -312,7 +312,7 @@ }, { "model": "reports.countryprogramme", - "pk": 14934, + "pk": 7, "fields": { "name": "Country Programme 6", "wbs": "0000/A0/06", @@ -326,31 +326,31 @@ "result": { "master": { "model": "partners.interventionresultlink", - "pk": 4584, + "pk": 1, "fields": { - "created": "2018-08-23T15:20:15.408Z", - "modified": "2018-08-23T15:20:15.409Z", - "intervention": 13916, - "cp_output": 6529, + "created": "2018-09-10T10:17:06.201Z", + "modified": "2018-09-10T10:17:06.202Z", + "intervention": 4, + "cp_output": 1, "ram_indicators": [] } }, "deps": [ { "model": "partners.intervention", - "pk": 13916, + "pk": 4, "fields": { - "created": "2018-08-23T15:20:15.398Z", - "modified": "2018-08-23T15:20:15.399Z", + "created": "2018-09-10T10:17:06.191Z", + "modified": "2018-09-10T10:17:06.192Z", "document_type": "", - "agreement": 16003, + "agreement": 8, "country_programme": null, - "number": "TST/PCA201816003/201813916", + "number": "TST/PCA20188/20184", "title": "Intervention Title 3", "status": "draft", "start": null, "end": null, - "submission_date": "2018-08-23", + "submission_date": "2018-09-10", "submission_date_prc": null, "reference_number_year": 2018, "review_date_prc": null, @@ -375,22 +375,22 @@ }, { "model": "partners.agreement", - "pk": 16003, + "pk": 8, "fields": { - "created": "2018-08-23T15:20:15.395Z", - "modified": "2018-08-23T15:20:15.397Z", - "partner": 15303, - "country_programme": 14935, + "created": "2018-09-10T10:17:06.182Z", + "modified": "2018-09-10T10:17:06.190Z", + "partner": 8, + "country_programme": 8, "agreement_type": "PCA", - "agreement_number": "TST/PCA201816003", - "attached_agreement": "test/file_attachments/partner_organization/15303/agreements/test_file.pdf", - "start": "2018-08-23", + "agreement_number": "TST/PCA20188", + "attached_agreement": "", + "start": "2018-09-10", "end": "2018-12-31", "reference_number_year": 2018, "special_conditions_pca": false, - "signed_by_unicef_date": "2018-08-23", + "signed_by_unicef_date": "2018-09-10", "signed_by": null, - "signed_by_partner_date": "2018-08-23", + "signed_by_partner_date": "2018-09-10", "partner_manager": null, "status": "signed", "authorized_officers": [] @@ -398,10 +398,10 @@ }, { "model": "partners.partnerorganization", - "pk": 15303, + "pk": 8, "fields": { - "created": "2018-08-23T15:20:15.391Z", - "modified": "2018-08-23T15:20:15.393Z", + "created": "2018-09-10T10:17:06.178Z", + "modified": "2018-09-10T10:17:06.180Z", "partner_type": "", "cso_type": null, "name": "Partner 7", @@ -478,7 +478,7 @@ }, { "model": "reports.countryprogramme", - "pk": 14935, + "pk": 8, "fields": { "name": "Country Programme 7", "wbs": "0000/A0/07", @@ -489,10 +489,10 @@ }, { "model": "reports.result", - "pk": 6529, + "pk": 1, "fields": { "country_programme": null, - "result_type": 6097, + "result_type": 1, "sector": null, "name": "Result 0", "code": null, @@ -510,8 +510,8 @@ "activity_focus_name": null, "hidden": false, "ram": false, - "created": "2018-08-23T15:20:15.403Z", - "modified": "2018-08-23T15:20:15.407Z", + "created": "2018-09-10T10:17:06.196Z", + "modified": "2018-09-10T10:17:06.199Z", "lft": 1, "rght": 2, "tree_id": 1, @@ -520,7 +520,7 @@ }, { "model": "reports.resulttype", - "pk": 6097, + "pk": 1, "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 27cd9e6e0d..f149053fb4 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 @@ -20,8 +20,8 @@ }, "data": [ { - "id": 13916, - "number": "TST/PCA201816003/201813916", + "id": 4, + "number": "TST/PCA20188/20184", "document_type": "", "partner_name": "Partner 7", "status": "draft", @@ -37,7 +37,7 @@ "sections": [], "section_names": [], "cp_outputs": [ - 6529 + 1 ], "unicef_focal_points": [], "frs_total_intervention_amt": null, @@ -60,8 +60,8 @@ "grants": [] }, { - "id": 13915, - "number": "TST/PCA201816002/201813915", + "id": 3, + "number": "TST/PCA20187/20183", "document_type": "", "partner_name": "Partner 6", "status": "draft", @@ -99,7 +99,7 @@ }, { "id": 101, - "number": "TST/PCA201816001/2018101", + "number": "TST/PCA20186/2018101", "document_type": "", "partner_name": "Partner 5", "status": "draft", 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 70a83f2c61..e463fd38fd 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 @@ -22,9 +22,9 @@ "id": 101, "frs": [], "partner": "Partner 5", - "agreement": 16001, + "agreement": 6, "document_type": "", - "number": "TST/PCA201816001/2018101", + "number": "TST/PCA20186/2018101", "prc_review_document_file": null, "frs_details": { "frs": [], @@ -44,7 +44,7 @@ "end": null, "submission_date_prc": null, "review_date_prc": null, - "submission_date": "2018-08-23", + "submission_date": "2018-09-10", "prc_review_document": null, "submitted_to_prc": false, "signed_pd_document": null, @@ -56,8 +56,8 @@ "offices": [], "population_focus": null, "signed_by_partner_date": null, - "created": "2018-08-23T15:20:15.353000Z", - "modified": "2018-08-23T15:20:15.356000Z", + "created": "2018-09-10T10:17:06.137000Z", + "modified": "2018-09-10T10:17:06.140000Z", "planned_budget": null, "result_links": [], "country_programme": null, @@ -67,178 +67,178 @@ "attachments": [], "permissions": { "edit": { - "partner_authorized_officer_signatory_id": true, - "modified": true, + "activity": true, + "flat_locations": true, "country_programme_id": true, - "actionpoint": true, - "document_type": false, - "activation_letter_attachment": false, - "unicef_signatory_id": true, - "signed_pd_attachment": false, - "in_amendment": false, + "travel_activities": true, + "reporting_requirements": false, "sections": false, - "end": false, - "unicef_focal_points": false, + "status": true, + "amendments": false, + "result_links": false, + "title": false, + "country_programme": false, + "submission_date_prc": false, "unicef_signatory": false, "termination_doc": false, - "offices": false, - "planned_budget": false, - "submission_date": false, - "title": false, - "number": false, - "signed_by_partner_date": false, - "activation_letter": false, + "planned_visits": false, + "agreement": false, + "actionpoint": true, "attachments": true, + "planned_budget": false, + "agreement_id": true, + "reference_number_year": true, + "sector_locations": true, + "unicef_signatory_id": true, "partner_focal_points": false, - "status": true, - "amendments": false, + "signed_pd_attachment": false, "start": false, - "metadata": true, - "agreement_id": true, - "activity": true, "review_date_prc": false, - "reporting_periods": true, - "country_programme": false, + "frs": false, + "partner_authorized_officer_signatory": false, + "unicef_focal_points": false, + "signed_by_partner_date": false, + "created": true, + "termination_doc_attachment": true, + "offices": false, + "signed_by_unicef_date": false, + "special_reporting_requirements": true, + "prc_review_attachment": false, + "engagement": true, + "contingency_pd": false, + "activation_letter_attachment": false, + "end": false, + "activation_letter": false, "prc_review_document": false, + "number": false, "population_focus": true, + "partner_authorized_officer_signatory_id": true, + "in_amendment": false, "signed_pd_document": false, - "travel_activities": true, - "submission_date_prc": false, - "reporting_requirements": false, - "flat_locations": true, - "reference_number_year": true, - "signed_by_unicef_date": false, - "agreement": false, - "created": true, - "partner_authorized_officer_signatory": false, - "contingency_pd": false, - "prc_review_attachment": false, - "result_links": false, - "frs": false, + "submission_date": false, + "metadata": true, + "modified": true, + "reporting_periods": true, + "document_type": false, "id": true, - "planned_visits": false, - "termination_doc_attachment": true, - "sector_locations": true, - "engagement": true, - "special_reporting_requirements": true, "sections_present": true }, "required": { - "partner_authorized_officer_signatory_id": false, - "modified": false, + "activity": false, + "flat_locations": false, "country_programme_id": false, - "actionpoint": false, - "document_type": true, - "activation_letter_attachment": false, - "unicef_signatory_id": false, - "signed_pd_attachment": false, - "in_amendment": false, + "travel_activities": false, + "reporting_requirements": false, "sections": false, - "end": false, - "unicef_focal_points": false, + "status": false, + "amendments": false, + "result_links": false, + "title": true, + "country_programme": false, + "submission_date_prc": false, "unicef_signatory": false, "termination_doc": false, - "offices": false, - "planned_budget": false, - "submission_date": false, - "title": true, - "number": true, - "signed_by_partner_date": false, - "activation_letter": false, + "planned_visits": false, + "agreement": true, + "actionpoint": false, "attachments": false, + "planned_budget": false, + "agreement_id": false, + "reference_number_year": true, + "sector_locations": false, + "unicef_signatory_id": false, "partner_focal_points": false, - "status": false, - "amendments": false, + "signed_pd_attachment": false, "start": false, - "metadata": false, - "agreement_id": false, - "activity": false, "review_date_prc": false, - "reporting_periods": false, - "country_programme": false, + "frs": false, + "partner_authorized_officer_signatory": false, + "unicef_focal_points": false, + "signed_by_partner_date": false, + "created": false, + "termination_doc_attachment": false, + "offices": false, + "signed_by_unicef_date": false, + "special_reporting_requirements": false, + "prc_review_attachment": false, + "engagement": false, + "contingency_pd": false, + "activation_letter_attachment": false, + "end": false, + "activation_letter": false, "prc_review_document": false, + "number": true, "population_focus": false, + "partner_authorized_officer_signatory_id": false, + "in_amendment": false, "signed_pd_document": false, - "travel_activities": false, - "submission_date_prc": false, - "reporting_requirements": false, - "flat_locations": false, - "reference_number_year": true, - "signed_by_unicef_date": false, - "agreement": true, - "created": false, - "partner_authorized_officer_signatory": false, - "contingency_pd": false, - "prc_review_attachment": false, - "result_links": false, - "frs": false, + "submission_date": false, + "metadata": false, + "modified": false, + "reporting_periods": false, + "document_type": true, "id": false, - "planned_visits": false, - "termination_doc_attachment": false, - "sector_locations": false, - "engagement": false, - "special_reporting_requirements": false, "sections_present": false }, "view": { - "partner_authorized_officer_signatory_id": true, - "modified": true, + "activity": true, + "flat_locations": false, "country_programme_id": true, - "actionpoint": true, - "document_type": false, - "activation_letter_attachment": false, - "unicef_signatory_id": true, - "signed_pd_attachment": false, - "in_amendment": false, + "travel_activities": true, + "reporting_requirements": false, "sections": false, - "end": false, - "unicef_focal_points": false, + "status": true, + "amendments": false, + "result_links": true, + "title": false, + "country_programme": false, + "submission_date_prc": false, "unicef_signatory": false, "termination_doc": false, - "offices": false, - "planned_budget": false, - "submission_date": false, - "title": false, - "number": false, - "signed_by_partner_date": false, - "activation_letter": false, + "planned_visits": false, + "agreement": false, + "actionpoint": true, "attachments": true, + "planned_budget": false, + "agreement_id": true, + "reference_number_year": true, + "sector_locations": true, + "unicef_signatory_id": true, "partner_focal_points": false, - "status": true, - "amendments": false, + "signed_pd_attachment": false, "start": false, - "metadata": true, - "agreement_id": true, - "activity": true, "review_date_prc": false, - "reporting_periods": true, - "country_programme": false, + "frs": false, + "partner_authorized_officer_signatory": false, + "unicef_focal_points": false, + "signed_by_partner_date": false, + "created": true, + "termination_doc_attachment": true, + "offices": false, + "signed_by_unicef_date": false, + "special_reporting_requirements": true, + "prc_review_attachment": false, + "engagement": true, + "contingency_pd": false, + "activation_letter_attachment": false, + "end": false, + "activation_letter": false, "prc_review_document": false, + "number": false, "population_focus": true, + "partner_authorized_officer_signatory_id": true, + "in_amendment": false, "signed_pd_document": false, - "travel_activities": true, - "submission_date_prc": false, - "reporting_requirements": false, - "flat_locations": false, - "reference_number_year": true, - "signed_by_unicef_date": false, - "agreement": false, - "created": true, - "partner_authorized_officer_signatory": false, - "contingency_pd": false, - "prc_review_attachment": false, - "result_links": true, - "frs": false, + "submission_date": false, + "metadata": true, + "modified": true, + "reporting_periods": true, + "document_type": false, "id": true, - "planned_visits": false, - "termination_doc_attachment": true, - "sector_locations": true, - "engagement": true, - "special_reporting_requirements": true, "sections_present": false } }, - "partner_id": "15301", + "partner_id": "6", "sections": [], "planned_visits": [], "locations": [], 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 0e9a84cb20..8d9501615c 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 @@ -20,20 +20,20 @@ }, "data": [ { - "id": 674, + "id": 1, "amendment_number": "1", - "signed_amendment_file": "http://testserver/media/test/file_attachments/partner_organization/15302/15302/agreements/16002/interventions/13915/amendments/None/test_file.pdf", + "signed_amendment_file": "http://testserver/media/test/file_attachments/partner_organization/7/7/agreements/7/interventions/3/amendments/None/test_file.pdf", "signed_amendment_attachment": null, "internal_prc_review": null, - "created": "2018-08-23T15:20:15.369000Z", - "modified": "2018-08-23T15:20:15.388000Z", + "created": "2018-09-10T10:17:06.159000Z", + "modified": "2018-09-10T10:17:06.176000Z", "types": [ - "Change in clause" + "Change banking info" ], - "other_description": "nsVZMcJfJpIZWElxzhLSmnvQaXJLijVgcfvqdQMBcBXQHtGQIR", - "signed_date": "2018-08-23", - "signed_amendment": "http://testserver/media/test/file_attachments/partner_organization/15302/15302/agreements/16002/interventions/13915/amendments/None/test_file.pdf", - "intervention": 13915 + "other_description": "GCbYeGGQeTyXjnWsiAJoQtAFpPVsIdPQecJebpiCTsDumQgMCy", + "signed_date": "2018-09-10", + "signed_amendment": "http://testserver/media/test/file_attachments/partner_organization/7/7/agreements/7/interventions/3/amendments/None/test_file.pdf", + "intervention": 3 } ], "content_type": null 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 dbe430090b..77d3c80391 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 @@ -20,7 +20,7 @@ }, "data": [ { - "intervention": 13916, + "intervention": 4, "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 856eb74135..77e5c37b7b 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 @@ -20,12 +20,12 @@ }, "data": [ { - "id": 13916, - "partner_id": "15303", + "id": 4, + "partner_id": "8", "partner_name": "Partner 7", - "agreement": 16003, + "agreement": 8, "document_type": "", - "number": "TST/PCA201816003/201813916", + "number": "TST/PCA20188/20184", "title": "Intervention Title 3", "status": "draft", "start": null, @@ -34,19 +34,17 @@ "sections": [], "locations": [], "unicef_focal_points": [], - "donors": null, - "donor_codes": null, - "grants": null, "results": "Result 0", - "clusters": null + "clusters": null, + "frs": [] }, { - "id": 13915, - "partner_id": "15302", + "id": 3, + "partner_id": "7", "partner_name": "Partner 6", - "agreement": 16002, + "agreement": 7, "document_type": "", - "number": "TST/PCA201816002/201813915", + "number": "TST/PCA20187/20183", "title": "Intervention Title 2", "status": "draft", "start": null, @@ -55,19 +53,17 @@ "sections": [], "locations": [], "unicef_focal_points": [], - "donors": null, - "donor_codes": null, - "grants": null, - "results": null, - "clusters": null + "results": "", + "clusters": null, + "frs": [] }, { "id": 101, - "partner_id": "15301", + "partner_id": "6", "partner_name": "Partner 5", - "agreement": 16001, + "agreement": 6, "document_type": "", - "number": "TST/PCA201816001/2018101", + "number": "TST/PCA20186/2018101", "title": "Intervention Title 1", "status": "draft", "start": null, @@ -76,11 +72,9 @@ "sections": [], "locations": [], "unicef_focal_points": [], - "donors": null, - "donor_codes": null, - "grants": null, - "results": null, - "clusters": null + "results": "", + "clusters": null, + "frs": [] } ], "content_type": null diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/fixtures.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/fixtures.json index d3fccac175..6d888ff08f 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/fixtures.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/fixtures.json @@ -4,8 +4,8 @@ "model": "partners.partnerorganization", "pk": 101, "fields": { - "created": "2018-08-23T15:20:16.677Z", - "modified": "2018-08-23T15:20:16.681Z", + "created": "2018-09-10T10:17:07.409Z", + "modified": "2018-09-10T10:17:07.412Z", "partner_type": "Civil Society Organization", "cso_type": "International", "name": "Partner 8", @@ -85,10 +85,10 @@ "partner_not_programmatic_visit_compliant": { "master": { "model": "partners.partnerorganization", - "pk": 15304, + "pk": 9, "fields": { - "created": "2018-08-23T15:20:16.683Z", - "modified": "2018-08-23T15:20:16.686Z", + "created": "2018-09-10T10:17:07.414Z", + "modified": "2018-09-10T10:17:07.416Z", "partner_type": "", "cso_type": null, "name": "Partner 9", diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_.response.json index a934e0a56a..635674d62c 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_.response.json @@ -53,7 +53,7 @@ "city": null, "postal_code": null, "country": null, - "id": 15304, + "id": 9, "vendor_number": null, "deleted_flag": false, "blocked": false, diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_101_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_101_.response.json index 4db2f9242e..ca0e03b5b7 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_101_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_101_.response.json @@ -78,8 +78,8 @@ "expiring_assessment_flag": false, "approaching_threshold_flag": false }, - "created": "2018-08-23T15:20:16.677000Z", - "modified": "2018-08-23T15:20:16.681000Z", + "created": "2018-09-10T10:17:07.409000Z", + "modified": "2018-09-10T10:17:07.412000Z", "partner_type": "Civil Society Organization", "cso_type": "International", "name": "Partner 8", diff --git a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_not_programmatic_visit_.response.json b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_not_programmatic_visit_.response.json index 172e390e91..c74184b422 100644 --- a/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_not_programmatic_visit_.response.json +++ b/src/etools/applications/partners/tests/_api_checker/etools.applications.partners.tests.test_api/TestPartners/get__api_v2_partners_not_programmatic_visit_.response.json @@ -26,7 +26,7 @@ "city": null, "postal_code": null, "country": null, - "id": 15304, + "id": 9, "vendor_number": null, "deleted_flag": false, "blocked": false, From 1bada61c777874e72ff50de4f1403a9f9adcae20 Mon Sep 17 00:00:00 2001 From: robertavram Date: Mon, 10 Sep 2018 14:53:43 -0400 Subject: [PATCH 09/42] Add endpoint for ram indicators Fixes [CH 7155] --- .../partners/serializers/interventions_v2.py | 14 ++++++++++++++ src/etools/applications/partners/urls_v2.py | 10 +++++++--- .../partners/views/interventions_v2.py | 17 +++++++++++++++++ src/etools/applications/reports/models.py | 7 +++++++ .../applications/reports/serializers/v2.py | 11 +++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 6d5893b456..b0802afb5a 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -33,6 +33,7 @@ LowerResultCUSerializer, LowerResultSerializer, ReportingRequirementSerializer, + RAMIndicatorSerializer ) @@ -693,6 +694,19 @@ class Meta: fields = ("reporting_requirements", ) +class InterventionRAMIndicatorsListSerializer(serializers.ModelSerializer): + + ram_indicators = RAMIndicatorSerializer( + many=True, + read_only=True + ) + cp_output_name = serializers.CharField(source="cp_output.output_name") + + class Meta: + model = InterventionResultLink + fields = ("ram_indicators", "cp_output_name") + + class InterventionReportingRequirementCreateSerializer(serializers.ModelSerializer): report_type = serializers.ChoiceField( choices=ReportingRequirement.TYPE_CHOICES diff --git a/src/etools/applications/partners/urls_v2.py b/src/etools/applications/partners/urls_v2.py index f0bbfa292b..85fd237c95 100644 --- a/src/etools/applications/partners/urls_v2.py +++ b/src/etools/applications/partners/urls_v2.py @@ -18,13 +18,14 @@ InterventionLocationListAPIView, InterventionLowerResultListCreateView, InterventionLowerResultUpdateView, + InterventionRamIndicatorsView, InterventionReportingPeriodDetailView, InterventionReportingPeriodListCreateView, InterventionReportingRequirementView, InterventionResultLinkListCreateView, InterventionResultLinkUpdateView, InterventionResultListAPIView, - InterventionSectionLocationLinkListAPIView, ) + InterventionSectionLocationLinkListAPIView) from etools.applications.partners.views.partner_organization_v2 import ( PartnerOrganizationAddView, PartnerOrganizationAssessmentDeleteView, @@ -201,9 +202,12 @@ ), name='intervention-reporting-requirements' ), + url( + r'interventions/(?P\d+)/output_cp_indicators/(?P\d+)/$', + view=InterventionRamIndicatorsView.as_view(http_method_names=['get']), + name="interventions-output-cp-indicators", + ), - # TODO: figure this out - # url(r'^partners/interventions/$', view=InterventionsView.as_view()), url(r'^dropdowns/static/$', view=PMPStaticDropdownsListAPIView.as_view(http_method_names=['get']), name='dropdown-static-list'), diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index e062c3adbb..51c9e2683f 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -61,6 +61,7 @@ InterventionListMapSerializer, InterventionListSerializer, InterventionLocationExportSerializer, + InterventionRAMIndicatorsListSerializer, InterventionReportingPeriodSerializer, InterventionReportingRequirementCreateSerializer, InterventionReportingRequirementListSerializer, @@ -803,3 +804,19 @@ def post(self, request, intervention_pk, report_type, format=None): self.serializer_list_class(self.get_data()).data ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class InterventionRamIndicatorsView(APIView): + + serializer_class = InterventionRAMIndicatorsListSerializer + + def get(self, request, intervention_pk, cp_output_pk): + + intervention = get_object_or_404(Intervention, pk=intervention_pk) + + data = get_object_or_404(intervention.result_links.prefetch_related('cp_output__result_type', 'ram_indicators'), + cp_output__pk=cp_output_pk) + + return Response( + self.serializer_class(data).data + ) diff --git a/src/etools/applications/reports/models.py b/src/etools/applications/reports/models.py index dd9b0e0ac4..7e9cd08adc 100644 --- a/src/etools/applications/reports/models.py +++ b/src/etools/applications/reports/models.py @@ -761,6 +761,13 @@ def __str__(self): u'Target: {}'.format(self.target) if self.target else u'' ) + @property + def light_repr(self): + return u'{}{}'.format( + u'' if self.active else u'[Inactive] ', + self.name + ) + def save(self, *args, **kwargs): # Prevent from saving empty strings as code because of the unique together constraint if not self.code: diff --git a/src/etools/applications/reports/serializers/v2.py b/src/etools/applications/reports/serializers/v2.py index 44bf5e6472..9186bf6aeb 100644 --- a/src/etools/applications/reports/serializers/v2.py +++ b/src/etools/applications/reports/serializers/v2.py @@ -296,6 +296,17 @@ class Meta: fields = "__all__" +class RAMIndicatorSerializer(serializers.ModelSerializer): + indicator_name = serializers.SerializerMethodField() + + def get_indicator_name(self, obj): + return obj.light_repr + + class Meta: + model = Indicator + fields = ("indicator_name",) + + class ReportingRequirementSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) From af76517f918c08499957be4a13cbe65353a34d1e Mon Sep 17 00:00:00 2001 From: ntrncic Date: Tue, 11 Sep 2018 15:22:02 -0400 Subject: [PATCH 10/42] fix users test --- src/etools/applications/users/tests/test_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/users/tests/test_tasks.py b/src/etools/applications/users/tests/test_tasks.py index 83035db162..45800ef5bd 100644 --- a/src/etools/applications/users/tests/test_tasks.py +++ b/src/etools/applications/users/tests/test_tasks.py @@ -157,7 +157,7 @@ def test_create_or_update_user_exists(self): "companyName": "UNICEF", }) - self.assertEqual(res, {'processed': 1, 'created': 0, 'updated': 0, 'skipped': 0, 'errors': 1}) + self.assertEqual(res, {'processed': 1, 'created': 0, 'updated': 0, 'skipped': 0, 'errors': 0}) user = get_user_model().objects.get(email=email) self.assertIn(self.group, user.groups.all()) From b6cf84b7ee3d154ce799f312ca49e76186bad6cd Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 12:56:54 +0300 Subject: [PATCH 11/42] removed useless ExportViewSetDataMixin in favor to view actions --- .../applications/audit/serializers/export.py | 4 +- .../applications/audit/tests/test_views.py | 29 ++++++++- src/etools/applications/audit/views.py | 65 ++++++++++++------- src/etools/applications/utils/common/views.py | 41 ------------ 4 files changed, 69 insertions(+), 70 deletions(-) diff --git a/src/etools/applications/audit/serializers/export.py b/src/etools/applications/audit/serializers/export.py index dc63e95357..c79f5d833e 100644 --- a/src/etools/applications/audit/serializers/export.py +++ b/src/etools/applications/audit/serializers/export.py @@ -298,7 +298,7 @@ def get_subject_area(self, obj): weaknesses = serializer.to_representation(serializer.get_attribute(instance=obj)) return OrderedDict( - (b['id'], ', '.join([risk['value_display'] for risk in b['risks']]) or '-') + (b['id'], ', '.join([str(risk['value_display']) for risk in b['risks']]) or '-') for b in weaknesses['blueprints'] ) @@ -339,7 +339,7 @@ def get_questionnaire(self, obj): ) -class SpecialAuditDetailPDFSerializer(EngagementBaseDetailCSVSerializer): +class SpecialAuditDetailCSVSerializer(EngagementBaseDetailCSVSerializer): """ """ diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 79be38de36..ccf702b6df 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -376,6 +376,32 @@ def test_hact_view(self): self.assertEqual(len(response.data), 1) self.assertNotEqual(response.data[0], {}) + def test_csv_view(self): + AuditFactory() + MicroAssessmentFactory() + SpotCheckFactory() + + response = self.forced_auth_req( + 'get', + '/api/audit/engagements/csv/', + user=self.unicef_user, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('text/csv', response['Content-Type']) + + def test_staff_spot_checks_csv_view(self): + engagement = StaffSpotCheckFactory() + + response = self.forced_auth_req( + 'get', + '/api/audit/staff-spot-checks/csv/'.format(engagement.id), + user=self.unicef_user, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('text/csv', response['Content-Type']) + class BaseTestEngagementsCreateViewSet(EngagementTransitionsTestCaseMixin): endpoint = 'engagements' @@ -1081,9 +1107,8 @@ def setUpTestData(cls): def test_csv_view(self): response = self.forced_auth_req( 'get', - '/api/audit/micro-assessments/{}/'.format(self.engagement.id), + '/api/audit/micro-assessments/{}/csv/'.format(self.engagement.id), user=self.unicef_user, - data={'format': 'csv'} ) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index b8a5779bbd..8d85ca6ece 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -2,6 +2,7 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Prefetch from django.http import Http404 +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend @@ -76,7 +77,7 @@ AuditPDFSerializer, MicroAssessmentDetailCSVSerializer, MicroAssessmentPDFSerializer, - SpecialAuditDetailPDFSerializer, + SpecialAuditDetailCSVSerializer, SpecialAuditPDFSerializer, SpotCheckDetailCSVSerializer, SpotCheckPDFSerializer, @@ -86,13 +87,11 @@ from etools.applications.permissions2.conditions import ObjectStatusCondition from etools.applications.permissions2.drf_permissions import get_permission_for_targets, NestedPermission from etools.applications.permissions2.views import PermittedFSMActionMixin, PermittedSerializerMixin -from etools.applications.utils.common.views import ExportViewSetDataMixin from etools.applications.vision.adapters.purchase_order import POSynchronizer class BaseAuditViewSet( SafeTenantViewSetMixin, - ExportViewSetDataMixin, MultiSerializerViewSetMixin, PermittedSerializerMixin, ): @@ -131,7 +130,6 @@ class AuditorFirmViewSet( serializer_action_classes = { 'list': AuditorFirmLightSerializer } - export_serializer_class = AuditorFirmExportSerializer renderer_classes = [JSONRenderer, AuditorFirmCSVRenderer] filter_backends = (SearchFilter, OrderingFilter, DjangoFilterBackend) search_fields = ('name', 'email') @@ -251,8 +249,6 @@ class EngagementViewSet( } metadata_class = AuditPermissionBasedMetadata - export_serializer_class = EngagementExportSerializer - export_filename = 'engagements' renderer_classes = [JSONRenderer, EngagementCSVRenderer] filter_backends = ( @@ -314,8 +310,7 @@ def get_queryset(self): 'partner', Prefetch('agreement', PurchaseOrder.objects.prefetch_related('auditor_firm')) ) - if self.action == 'list': - queryset = queryset.filter(agreement__auditor_firm__unicef_users_allowed=self.unicef_engagements) + queryset = queryset.filter(agreement__auditor_firm__unicef_users_allowed=self.unicef_engagements) return queryset @@ -380,6 +375,15 @@ def export_pdf(self, request, *args, **kwargs): filename='engagement_{}.pdf'.format(obj.unique_id), ) + @action(detail=False, methods=['get'], url_path='csv', renderer_classes=[EngagementCSVRenderer]) + def export_list_csv(self, request, *args, **kwargs): + engagements = self.get_queryset() + serializer = EngagementExportSerializer(engagements, many=True) + + return Response(serializer.data, headers={ + 'Content-Disposition': 'attachment;filename=engagements_{}.csv'.format(timezone.now().date()) + }) + class EngagementManagementMixin( mixins.RetrieveModelMixin, @@ -387,37 +391,46 @@ class EngagementManagementMixin( mixins.DestroyModelMixin, PermittedFSMActionMixin, ): - def get_export_filename(self, format=None): - instance = self.get_object() + csv_export_serializer = EngagementExportSerializer - if instance: - return '{}.{}'.format(instance.unique_id, (format or '').lower()) + @action(detail=True, methods=['get'], url_path='csv', renderer_classes=[EngagementCSVRenderer]) + def export_csv(self, request, *args, **kwargs): + engagement = self.get_object() + serializer = self.csv_export_serializer(engagement) - return super().get_export_filename(format=format) + return Response(serializer.data, headers={ + 'Content-Disposition': 'attachment;filename={}.csv'.format(engagement.unique_id) + }) class MicroAssessmentViewSet(EngagementManagementMixin, EngagementViewSet): queryset = MicroAssessment.objects.all() serializer_class = MicroAssessmentSerializer - export_serializer_class = MicroAssessmentDetailCSVSerializer - renderer_classes = [JSONRenderer, MicroAssessmentDetailCSVRenderer] - export_filename = 'microassessments' + csv_export_serializer = MicroAssessmentDetailCSVSerializer + + @action(detail=True, methods=['get'], url_path='csv', renderer_classes=[MicroAssessmentDetailCSVRenderer]) + def export_csv(self, request, *args, **kwargs): + return super().export_csv(request, *args, **kwargs) class AuditViewSet(EngagementManagementMixin, EngagementViewSet): queryset = Audit.objects.all() serializer_class = AuditSerializer - export_serializer_class = AuditDetailCSVSerializer - renderer_classes = [JSONRenderer, AuditDetailCSVRenderer] - export_filename = 'audits' + csv_export_serializer = AuditDetailCSVSerializer + + @action(detail=True, methods=['get'], url_path='csv', renderer_classes=[AuditDetailCSVRenderer]) + def export_csv(self, request, *args, **kwargs): + return super().export_csv(request, *args, **kwargs) class SpotCheckViewSet(EngagementManagementMixin, EngagementViewSet): queryset = SpotCheck.objects.all() serializer_class = SpotCheckSerializer - export_serializer_class = SpotCheckDetailCSVSerializer - renderer_classes = [JSONRenderer, SpotCheckDetailCSVRenderer] - export_filename = 'spot-checks' + csv_export_serializer = SpotCheckDetailCSVSerializer + + @action(detail=True, methods=['get'], url_path='csv', renderer_classes=[SpotCheckDetailCSVRenderer]) + def export_csv(self, request, *args, **kwargs): + return super().export_csv(request, *args, **kwargs) class StaffSpotCheckViewSet(SpotCheckViewSet): @@ -426,14 +439,16 @@ class StaffSpotCheckViewSet(SpotCheckViewSet): serializer_action_classes = { 'list': StaffSpotCheckListSerializer } - export_filename = 'staff-spot-checks' class SpecialAuditViewSet(EngagementManagementMixin, EngagementViewSet): queryset = SpecialAudit.objects.all() serializer_class = SpecialAuditSerializer - export_serializer_class = SpecialAuditDetailPDFSerializer - renderer_classes = [JSONRenderer, SpecialAuditDetailCSVRenderer] + csv_export_serializer = SpecialAuditDetailCSVSerializer + + @action(detail=True, methods=['get'], url_path='csv', renderer_classes=[SpecialAuditDetailCSVRenderer]) + def export_csv(self, request, *args, **kwargs): + return super().export_csv(request, *args, **kwargs) class AuditorStaffMembersViewSet( diff --git a/src/etools/applications/utils/common/views.py b/src/etools/applications/utils/common/views.py index 78ddae7e18..e7748805c1 100644 --- a/src/etools/applications/utils/common/views.py +++ b/src/etools/applications/utils/common/views.py @@ -4,47 +4,6 @@ from django_fsm import can_proceed, has_transition_perm from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied, ValidationError -from rest_framework.serializers import Serializer - - -class ExportViewSetDataMixin(object): - export_serializer_class = None - allowed_formats = ['csv', ] - export_filename = '' - - def get_export_filename(self, format=None): - return self.export_filename + '.' + (format or '').lower() - - def get_export_serializer_class(self, export_format=None): - if isinstance(self.export_serializer_class, Serializer): - return self.export_serializer_class - - if isinstance(self.export_serializer_class, dict): - serializer_class = self.export_serializer_class.get( - export_format, - self.export_serializer_class['default'] - ) - if not serializer_class: - raise KeyError - return serializer_class - - return self.export_serializer_class - - def get_serializer_class(self): - if self.request.method == "GET": - query_params = self.request.query_params - export_format = query_params.get('format') - if export_format and self.export_serializer_class: - return self.get_export_serializer_class(export_format=export_format) - return super(ExportViewSetDataMixin, self).get_serializer_class() - - def dispatch(self, request, *args, **kwargs): - response = super(ExportViewSetDataMixin, self).dispatch(request, *args, **kwargs) - if self.request.method == "GET" and 'format' in self.request.query_params.keys(): - response['Content-Disposition'] = "attachment;filename={}".format( - self.get_export_filename(format=self.request.query_params.get('format')) - ) - return response class FSMTransitionActionMixin(object): From 5c5c61ec89393f1a5826a8a610f306232249639d Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 13:03:13 +0300 Subject: [PATCH 12/42] fixed nested paths for staff spot checks --- src/etools/applications/audit/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index 8d85ca6ece..d702a64f09 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -310,7 +310,8 @@ def get_queryset(self): 'partner', Prefetch('agreement', PurchaseOrder.objects.prefetch_related('auditor_firm')) ) - queryset = queryset.filter(agreement__auditor_firm__unicef_users_allowed=self.unicef_engagements) + if self.action in ['list', 'export_list_csv']: + queryset = queryset.filter(agreement__auditor_firm__unicef_users_allowed=self.unicef_engagements) return queryset From 83e23326f5105777133aee3b6bcad47b78d2a578 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 13:13:26 +0300 Subject: [PATCH 13/42] removed unused import --- src/etools/applications/audit/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index d702a64f09..d9fcf57b22 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -50,7 +50,6 @@ ) from etools.applications.audit.purchase_order.models import AuditorFirm, AuditorStaffMember, PurchaseOrder from etools.applications.audit.serializers.auditor import ( - AuditorFirmExportSerializer, AuditorFirmLightSerializer, AuditorFirmSerializer, AuditorStaffMemberSerializer, From 3f41a6f3ca612d333229d0d4509cad50d72d7adf Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 13:18:34 +0300 Subject: [PATCH 14/42] merged defaults with changes from develop --- src/etools/applications/audit/serializers/auditor.py | 2 +- src/etools/applications/audit/serializers/engagement.py | 8 ++++---- src/etools/applications/audit/serializers/export.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/etools/applications/audit/serializers/auditor.py b/src/etools/applications/audit/serializers/auditor.py index 3aa8d57fde..d5278983a8 100644 --- a/src/etools/applications/audit/serializers/auditor.py +++ b/src/etools/applications/audit/serializers/auditor.py @@ -114,7 +114,7 @@ class Meta(WritableNestedSerializerMixin.Meta): class AuditUserSerializer(UserSerializer): auditor_firm = serializers.SerializerMethodField() hidden = serializers.SerializerMethodField() - staff_member_id = serializers.ReadOnlyField(source='purchase_order_auditorstaffmember_id') + staff_member_id = serializers.ReadOnlyField(source='purchase_order_auditorstaffmember.id') class Meta(UserSerializer.Meta): fields = UserSerializer.Meta.fields + ['id', 'auditor_firm', 'hidden', 'staff_member_id', ] diff --git a/src/etools/applications/audit/serializers/engagement.py b/src/etools/applications/audit/serializers/engagement.py index 1113af84a0..fb24a14a15 100644 --- a/src/etools/applications/audit/serializers/engagement.py +++ b/src/etools/applications/audit/serializers/engagement.py @@ -136,11 +136,11 @@ def create(self, validated_data): class EngagementExportSerializer(serializers.ModelSerializer): - agreement_number = serializers.ReadOnlyField(source='agreement.order_number', default='') + agreement_number = serializers.ReadOnlyField(source='agreement.order_number') engagement_type = serializers.ReadOnlyField(source='get_engagement_type_display') - partner_name = serializers.ReadOnlyField(source='partner.name', default='') - auditor_firm_vendor_number = serializers.ReadOnlyField(source='agreement.auditor_firm.vendor_number', default='') - auditor_firm_name = serializers.ReadOnlyField(source='agreement.auditor_firm.name', default='') + partner_name = serializers.ReadOnlyField(source='partner.name') + auditor_firm_vendor_number = serializers.ReadOnlyField(source='agreement.auditor_firm.vendor_number') + auditor_firm_name = serializers.ReadOnlyField(source='agreement.auditor_firm.name') status = serializers.ChoiceField( choices=Engagement.DISPLAY_STATUSES, source='displayed_status', diff --git a/src/etools/applications/audit/serializers/export.py b/src/etools/applications/audit/serializers/export.py index c79f5d833e..1a1638f138 100644 --- a/src/etools/applications/audit/serializers/export.py +++ b/src/etools/applications/audit/serializers/export.py @@ -69,8 +69,8 @@ def get_address(self, obj): class StaffMemberPDFSerializer(serializers.ModelSerializer): first_name = serializers.CharField(source='user.first_name') last_name = serializers.CharField(source='user.last_name') - job_title = serializers.CharField(source='user.profile.job_title', default='') - phone_number = serializers.CharField(source='user.profile.phone_number', default='') + job_title = serializers.CharField(source='user.profile.job_title') + phone_number = serializers.CharField(source='user.profile.phone_number') email = serializers.CharField(source='user.email') class Meta: @@ -84,8 +84,8 @@ class EngagementActionPointPDFSerializer(serializers.ModelSerializer): status = serializers.CharField(source='get_status_display') due_date = serializers.DateField(format='%d %b %Y') assigned_to = serializers.CharField(source='assigned_to.get_full_name') - office = serializers.CharField(source='office.name', default='') - section = serializers.CharField(source='section.name', default='') + office = serializers.CharField(source='office.name') + section = serializers.CharField(source='section.name') class Meta: model = EngagementActionPoint From 98ef8d8913a7b577cd1a265fff28d41e5910f1ca Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 13:21:46 +0300 Subject: [PATCH 15/42] activity can missing intervention --- src/etools/applications/tpm/export/serializers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/etools/applications/tpm/export/serializers.py b/src/etools/applications/tpm/export/serializers.py index 249b12103c..5563a6ea63 100644 --- a/src/etools/applications/tpm/export/serializers.py +++ b/src/etools/applications/tpm/export/serializers.py @@ -25,8 +25,8 @@ class TPMActivityExportSerializer(serializers.Serializer): section = serializers.CharField() cp_output = serializers.CharField() partner = serializers.CharField() - intervention = serializers.CharField(source='intervention.reference_number', default='') - pd_ssfa = serializers.CharField(source='intervention.title', default='') + intervention = serializers.CharField(source='intervention.reference_number', allow_null=True) + pd_ssfa = serializers.CharField(source='intervention.title', allow_null=True) locations = CommaSeparatedExportField() date = serializers.DateField(format='%d/%m/%Y') unicef_focal_points = UsersExportField() @@ -47,8 +47,8 @@ class TPMLocationExportSerializer(serializers.Serializer): section = serializers.CharField(source='activity.tpmactivity.section') cp_output = serializers.CharField(source='activity.tpmactivity.cp_output') partner = serializers.CharField(source='activity.tpmactivity.partner') - intervention = serializers.CharField(source='activity.tpmactivity.intervention.reference_number', default='') - pd_ssfa = serializers.CharField(source='activity.tpmactivity.intervention.title', default='') + intervention = serializers.CharField(source='activity.tpmactivity.intervention.reference_number', allow_null=True) + pd_ssfa = serializers.CharField(source='activity.tpmactivity.intervention.title', allow_null=True) location = serializers.CharField() date = serializers.DateField(source='activity.tpmactivity.date', format='%d/%m/%Y') unicef_focal_points = UsersExportField(source='activity.tpmactivity.unicef_focal_points') From ae375abb1a0c59fd6a9d914cee916dbc2f354785 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 13:57:21 +0300 Subject: [PATCH 16/42] tpm partner attachments shouldn't be editable by tpm or general unicef user --- src/etools/applications/tpm/tests/test_views.py | 15 +++++++++++++++ src/etools/applications/tpm/views.py | 3 +++ 2 files changed, 18 insertions(+) diff --git a/src/etools/applications/tpm/tests/test_views.py b/src/etools/applications/tpm/tests/test_views.py index 7bbb2736a1..15b2cc892a 100644 --- a/src/etools/applications/tpm/tests/test_views.py +++ b/src/etools/applications/tpm/tests/test_views.py @@ -610,6 +610,21 @@ def test_add(self): ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_not_editable_by_tpm(self): + partner = TPMPartnerFactory() + + response = self.forced_auth_req( + 'post', + reverse('tpm:partner-attachments-list', args=[partner.id]), + user=UserFactory(tpm=True, tpm_partner=partner), + request_format='multipart', + data={ + 'file_type': AttachmentFileTypeFactory(code='tpm_partner').id, + 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + } + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + class TestVisitReportAttachmentsView(TPMTestCaseMixin, BaseTenantTestCase): @classmethod diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index bd28f85b8e..74e1f13c92 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -496,6 +496,9 @@ class BaseTPMAttachmentsViewSet(BaseTPMViewSet, class PartnerAttachmentsViewSet(BaseTPMAttachmentsViewSet): serializer_class = TPMPartnerAttachmentsSerializer + permission_classes = BaseTPMViewSet.permission_classes + [ + get_permission_for_targets('tpmpartners.tpmpartner.attachments') + ] def get_view_name(self): return _('Attachments') From 58d022ea775af6c1c94f817b1f951bb50d525765 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 12 Sep 2018 14:12:31 +0300 Subject: [PATCH 17/42] visit shouldn't be visible for tpm if it was cancelled without assignment --- src/etools/applications/tpm/tests/test_views.py | 3 ++- src/etools/applications/tpm/views.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/tpm/tests/test_views.py b/src/etools/applications/tpm/tests/test_views.py index 7bbb2736a1..be98426151 100644 --- a/src/etools/applications/tpm/tests/test_views.py +++ b/src/etools/applications/tpm/tests/test_views.py @@ -42,8 +42,9 @@ def test_unicef_list_view(self): def test_tpm_list_view(self): TPMVisitFactory() + TPMVisitFactory(status='cancelled', date_of_assigned=None) - # drafts shouldn't be available for tpm + # drafts shouldn't be available for tpm, including cancelled from draft self._test_list_view(self.tpm_user, []) visit = TPMVisitFactory(status='assigned', diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index bd28f85b8e..4a3269b73d 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django.http import Http404 from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -335,7 +336,10 @@ def get_queryset(self): hasattr(self.request.user, 'tpmpartners_tpmpartnerstaffmember'): queryset = queryset.filter( tpm_partner=self.request.user.tpmpartners_tpmpartnerstaffmember.tpm_partner - ).exclude(status=TPMVisit.STATUSES.draft) + ).exclude( + Q(status=TPMVisit.STATUSES.draft) | + Q(status=TPMVisit.STATUSES.cancelled, date_of_assigned__isnull=True) # cancelled draft + ) else: queryset = queryset.none() From b1d142cdcdbe3d9bb02709795f8bb0332dccfbb4 Mon Sep 17 00:00:00 2001 From: robertavram Date: Wed, 12 Sep 2018 14:51:52 -0400 Subject: [PATCH 18/42] Add endpoint for PRP PD file retrieval --- .../applications/partners/permissions.py | 15 +++++++++++++ src/etools/applications/partners/prp_urls.py | 5 ++++- .../partners/serializers/prp_v1.py | 8 +++++++ .../applications/partners/views/prp_v1.py | 22 ++++++++++++++++--- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index f1d7c64d6a..beba25a37a 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -280,3 +280,18 @@ def has_permission(self, request, view): else: # This class shouldn't see methods other than GET and POST, but regardless the answer is 'no you may not'. return False + + +class ReadOnlyAPIUser(permissions.BasePermission): + '''Permission class for Views that only allow read and only for backend api users or superusers + GET users must be either (a) superusers or (b) in the Limited API group. + ''' + def has_permission(self, request, view): + if request.method in permissions.SAFE_METHODS: + if request.user.is_authenticated: + if request.user.is_superuser or is_user_in_groups(request.user, [READ_ONLY_API_GROUP_NAME]): + return True + return False + else: + # This class shouldn't see methods other than GET, but regardless the answer is 'no you may not'. + return False \ No newline at end of file diff --git a/src/etools/applications/partners/prp_urls.py b/src/etools/applications/partners/prp_urls.py index 3b733ec8f4..c4d636c289 100644 --- a/src/etools/applications/partners/prp_urls.py +++ b/src/etools/applications/partners/prp_urls.py @@ -2,7 +2,7 @@ from rest_framework.urlpatterns import format_suffix_patterns -from etools.applications.partners.views.prp_v1 import PRPInterventionListAPIView, PRPPartnerListAPIView +from etools.applications.partners.views.prp_v1 import PRPInterventionListAPIView, PRPPartnerListAPIView, PRPPDFileView app_name = 'partners' urlpatterns = ( @@ -12,5 +12,8 @@ url(r'^partners/$', view=PRPPartnerListAPIView.as_view(http_method_names=['get']), name='prp-partner-list'), + url(r'^get_pd_document/(?P\d+)/$', + view=PRPPDFileView.as_view(http_method_names=['get']), + name='prp-pd-document-get'), ) urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'csv']) diff --git a/src/etools/applications/partners/serializers/prp_v1.py b/src/etools/applications/partners/serializers/prp_v1.py index 7b5cfe7f31..2800608bf8 100644 --- a/src/etools/applications/partners/serializers/prp_v1.py +++ b/src/etools/applications/partners/serializers/prp_v1.py @@ -13,6 +13,14 @@ from etools.applications.reports.serializers.v1 import SectionSerializer +class InterventionPDFileSerializer(serializers.ModelSerializer): + signed_pd_document_file = serializers.FileField(source='signed_pd_document', read_only=True) + + class Meta: + model = Intervention + fields = ('signed_pd_document_file',) + + class PRPPartnerOrganizationListSerializer(serializers.ModelSerializer): rating = serializers.CharField(source='get_rating_display') unicef_vendor_number = serializers.CharField(source='vendor_number', read_only=True) diff --git a/src/etools/applications/partners/views/prp_v1.py b/src/etools/applications/partners/views/prp_v1.py index d2a66a92bd..cb61093f63 100644 --- a/src/etools/applications/partners/views/prp_v1.py +++ b/src/etools/applications/partners/views/prp_v1.py @@ -3,17 +3,33 @@ from django.db.models import Q -from rest_framework.generics import ListAPIView +from rest_framework.generics import ListAPIView, get_object_or_404 +from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework.pagination import LimitOffsetPagination + from etools.applications.partners.filters import PartnerScopeFilter from etools.applications.partners.models import Intervention, PartnerOrganization -from etools.applications.partners.permissions import ListCreateAPIMixedPermission +from etools.applications.partners.permissions import ListCreateAPIMixedPermission, ReadOnlyAPIUser from etools.applications.partners.serializers.prp_v1 import PRPInterventionListSerializer, \ - PRPPartnerOrganizationListSerializer + PRPPartnerOrganizationListSerializer, InterventionPDFileSerializer from etools.applications.partners.views.helpers import set_tenant_or_fail +class PRPPDFileView(APIView): + permission_classes = (ReadOnlyAPIUser,) + + def get(self, request, intervention_pk): + + workspace = request.query_params.get('workspace', None) + set_tenant_or_fail(workspace) + + intervention = get_object_or_404(Intervention, pk=intervention_pk) + + return Response(InterventionPDFileSerializer(intervention).data) + + class PRPInterventionPagination(LimitOffsetPagination): default_limit = 100 From a82000d6d7f63bfae65d8181eeb77f78d3f5e45c Mon Sep 17 00:00:00 2001 From: Marko Date: Thu, 13 Sep 2018 09:57:36 -0400 Subject: [PATCH 19/42] adds supervisor to travel export --- src/etools/applications/t2f/serializers/export.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index d82e3a7591..f2ac0166b1 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -26,6 +26,7 @@ class TravelActivityExportSerializer(serializers.Serializer): section = serializers.CharField(source='travel.section.name', read_only=True) office = serializers.CharField(source='travel.office.name', read_only=True) status = serializers.CharField(source='travel.status', read_only=True) + supervisor = serializers.CharField(source='travel.supervisor.get_full_name', read_only=True) trip_type = serializers.CharField(source='activity.travel_type', read_only=True) partner = serializers.CharField(source='activity.partner.name', read_only=True) partnership = serializers.CharField(source='activity.partnership.title', read_only=True) @@ -37,9 +38,9 @@ class TravelActivityExportSerializer(serializers.Serializer): primary_traveler_name = serializers.SerializerMethodField() class Meta: - fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'trip_type', 'partner', 'partnership', + fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'supervisor', 'trip_type', 'partner', 'partnership', 'results', 'locations', 'start_date', 'end_date', 'is_secondary_traveler', 'primary_traveler_name') - + def get_locations(self, obj): return ', '.join([l.name for l in obj.activity.locations.all()]) From 35343939c5846b196651fe5b339f89369bfb7709 Mon Sep 17 00:00:00 2001 From: Marko Date: Thu, 13 Sep 2018 10:14:43 -0400 Subject: [PATCH 20/42] whitespace --- src/etools/applications/t2f/serializers/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index f2ac0166b1..3f1e21c263 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -40,7 +40,7 @@ class TravelActivityExportSerializer(serializers.Serializer): class Meta: fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'supervisor', 'trip_type', 'partner', 'partnership', 'results', 'locations', 'start_date', 'end_date', 'is_secondary_traveler', 'primary_traveler_name') - + def get_locations(self, obj): return ', '.join([l.name for l in obj.activity.locations.all()]) From d6b97070d1ea88eaf26c0a013cb110656f8bf666 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 13 Sep 2018 10:21:13 -0400 Subject: [PATCH 21/42] Fix flake --- src/etools/applications/partners/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index beba25a37a..96890b574a 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -294,4 +294,4 @@ def has_permission(self, request, view): return False else: # This class shouldn't see methods other than GET, but regardless the answer is 'no you may not'. - return False \ No newline at end of file + return False From b65fe524dcc652081dd999d0d15b910bdf20c942 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Thu, 13 Sep 2018 17:25:57 +0300 Subject: [PATCH 22/42] inherited cancelled visit trait from assigned --- src/etools/applications/tpm/tests/factories.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/tpm/tests/factories.py b/src/etools/applications/tpm/tests/factories.py index 5c169bdeae..262177c524 100644 --- a/src/etools/applications/tpm/tests/factories.py +++ b/src/etools/applications/tpm/tests/factories.py @@ -239,7 +239,8 @@ class Params: date_of_assigned=factory.LazyFunction(timezone.now), ) - cancelled = factory.Trait( + cancelled = InheritedTrait( + assigned, status=TPMVisit.STATUSES.cancelled, date_of_cancelled=factory.LazyFunction(timezone.now), ) From a1dcc3cd3334548d30d6e249b8d16c3e8d0950f3 Mon Sep 17 00:00:00 2001 From: Marko Bilal Date: Thu, 13 Sep 2018 10:38:25 -0400 Subject: [PATCH 23/42] spaces again --- src/etools/applications/t2f/serializers/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index 3f1e21c263..5d2d2ba660 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -40,7 +40,7 @@ class TravelActivityExportSerializer(serializers.Serializer): class Meta: fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'supervisor', 'trip_type', 'partner', 'partnership', 'results', 'locations', 'start_date', 'end_date', 'is_secondary_traveler', 'primary_traveler_name') - + def get_locations(self, obj): return ', '.join([l.name for l in obj.activity.locations.all()]) From 4d729ca01cb5ecbe2b1f8d3e0904a85cd412b721 Mon Sep 17 00:00:00 2001 From: Marko Bilal Date: Thu, 13 Sep 2018 10:46:15 -0400 Subject: [PATCH 24/42] spaces? --- src/etools/applications/t2f/serializers/export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index 5d2d2ba660..de147c7046 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -40,7 +40,7 @@ class TravelActivityExportSerializer(serializers.Serializer): class Meta: fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'supervisor', 'trip_type', 'partner', 'partnership', 'results', 'locations', 'start_date', 'end_date', 'is_secondary_traveler', 'primary_traveler_name') - + def get_locations(self, obj): return ', '.join([l.name for l in obj.activity.locations.all()]) From c26792661d211272e17e047d55c81cba8cdf3b84 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 14 Sep 2018 09:29:41 -0400 Subject: [PATCH 25/42] adds supervisor test, adds prefetch for supervisor --- .../applications/t2f/tests/test_exports.py | 79 +++++++++++++------ src/etools/applications/t2f/views/exports.py | 2 +- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/etools/applications/t2f/tests/test_exports.py b/src/etools/applications/t2f/tests/test_exports.py index 44c6e44a4e..49a3aec396 100644 --- a/src/etools/applications/t2f/tests/test_exports.py +++ b/src/etools/applications/t2f/tests/test_exports.py @@ -41,7 +41,8 @@ class TravelExports(BaseTenantTestCase): @classmethod def setUpTestData(cls): cls.traveler = UserFactory(first_name='John', last_name='Doe') - cls.unicef_staff = UserFactory(first_name='Jakab', last_name='Gipsz', is_staff=True) + cls.unicef_staff = UserFactory( + first_name='Jakab', last_name='Gipsz', is_staff=True) def test_urls(self): export_url = reverse('t2f:travels:list:activity_export') @@ -97,14 +98,17 @@ def test_activity_export(self): last_name='Carter') user_lenox_lewis = UserFactory(first_name='Lenox', last_name='Lewis') + supervisor = UserFactory(first_name='ImYour', + last_name='Supervisor') + travel_1 = TravelFactory(reference_number='2016/1000', traveler=user_joe_smith, office=office, + supervisor=supervisor, section=section_health, start_date=datetime(2017, 11, 8, tzinfo=tz), end_date=datetime(2017, 11, 14, tzinfo=tz), ) - supervisor = UserFactory() travel_2 = TravelFactory(reference_number='2016/1211', supervisor=supervisor, traveler=user_alice_carter, @@ -119,7 +123,8 @@ def test_activity_export(self): # Create the activities finally activity_1 = TravelActivityFactory(travel_type=TravelType.PROGRAMME_MONITORING, - date=datetime(2016, 12, 3, tzinfo=UTC), + date=datetime( + 2016, 12, 3, tzinfo=UTC), result=result_A11, primary_traveler=user_joe_smith) activity_1.travels.add(travel_1) @@ -129,7 +134,8 @@ def test_activity_export(self): activity_1.save() activity_2 = TravelActivityFactory(travel_type=TravelType.PROGRAMME_MONITORING, - date=datetime(2016, 12, 4, tzinfo=UTC), + date=datetime( + 2016, 12, 4, tzinfo=UTC), result=result_A21, primary_traveler=user_lenox_lewis) activity_2.travels.add(travel_1) @@ -139,7 +145,8 @@ def test_activity_export(self): activity_2.save() activity_3 = TravelActivityFactory(travel_type=TravelType.MEETING, - date=datetime(2016, 12, 3, tzinfo=UTC), + date=datetime( + 2016, 12, 3, tzinfo=UTC), result=None, primary_traveler=user_joe_smith) activity_3.travels.add(travel_1) @@ -149,7 +156,8 @@ def test_activity_export(self): activity_3.save() activity_4 = TravelActivityFactory(travel_type=TravelType.SPOT_CHECK, - date=datetime(2016, 12, 6, tzinfo=UTC), + date=datetime( + 2016, 12, 6, tzinfo=UTC), result=None, primary_traveler=user_alice_carter) activity_4.travels.add(travel_2) @@ -158,14 +166,13 @@ def test_activity_export(self): activity_4.partnership = partnership_C1 activity_4.save() - with self.assertNumQueries(6): + with self.assertNumQueries(7): response = self.forced_auth_req('get', reverse('t2f:travels:list:activity_export'), user=self.unicef_staff) export_csv = csv.reader(StringIO(response.content.decode('utf-8'))) rows = [r for r in export_csv] self.assertEqual(len(rows), 5) - # check header self.assertEqual(rows[0], ['reference_number', @@ -173,6 +180,7 @@ def test_activity_export(self): 'office', 'section', 'status', + 'supervisor', 'trip_type', 'partner', 'partnership', @@ -189,6 +197,7 @@ def test_activity_export(self): 'Budapest', 'Health', 'planned', + 'ImYour Supervisor', 'Programmatic Visit', 'Partner A', 'Partnership A1', @@ -205,6 +214,7 @@ def test_activity_export(self): 'Budapest', 'Health', 'planned', + 'ImYour Supervisor', 'Programmatic Visit', 'Partner A', 'Partnership A2', @@ -221,6 +231,7 @@ def test_activity_export(self): 'Budapest', 'Health', 'planned', + 'ImYour Supervisor', 'Meeting', 'Partner B', 'Partnership B3', @@ -237,6 +248,7 @@ def test_activity_export(self): 'Budapest', 'Education', 'planned', + 'ImYour Supervisor', 'Spot Check', 'Partner C', 'Partnership C1', @@ -255,7 +267,8 @@ def test_finance_export(self): end_date=datetime(2016, 12, 5, tzinfo=UTC), mode_of_travel=[ModeOfTravel.PLANE, ModeOfTravel.CAR, ModeOfTravel.RAIL]) travel.expenses.all().delete() - ExpenseFactory(travel=travel, amount=Decimal('500'), currency=currency_usd) + ExpenseFactory(travel=travel, amount=Decimal( + '500'), currency=currency_usd) travel_2 = TravelFactory(traveler=self.traveler, supervisor=self.unicef_staff, @@ -263,7 +276,8 @@ def test_finance_export(self): end_date=datetime(2016, 12, 5, tzinfo=UTC), mode_of_travel=None) travel_2.expenses.all().delete() - ExpenseFactory(travel=travel_2, amount=Decimal('200'), currency=currency_usd) + ExpenseFactory(travel=travel_2, amount=Decimal( + '200'), currency=currency_usd) ExpenseFactory(travel=travel_2, amount=Decimal('100'), currency=None) with self.assertNumQueries(27): @@ -336,14 +350,17 @@ def test_travel_admin_export(self): airline_spiceair = PublicsAirlineCompanyFactory(name='SpiceAir') # First travel setup - travel_1 = TravelFactory(traveler=self.traveler, supervisor=self.unicef_staff) + travel_1 = TravelFactory( + traveler=self.traveler, supervisor=self.unicef_staff) travel_1.itinerary.all().delete() itinerary_item_1 = ItineraryItemFactory(travel=travel_1, origin='Origin1', destination='Origin2', - departure_date=datetime(2016, 12, 3, 11, tzinfo=UTC), - arrival_date=datetime(2016, 12, 3, 12, tzinfo=UTC), + departure_date=datetime( + 2016, 12, 3, 11, tzinfo=UTC), + arrival_date=datetime( + 2016, 12, 3, 12, tzinfo=UTC), mode_of_travel=ModeOfTravel.CAR, dsa_region=dsa_brd) itinerary_item_1.airlines.all().delete() @@ -351,8 +368,10 @@ def test_travel_admin_export(self): itinerary_item_2 = ItineraryItemFactory(travel=travel_1, origin='Origin2', destination='Origin3', - departure_date=datetime(2016, 12, 5, 11, tzinfo=UTC), - arrival_date=datetime(2016, 12, 5, 12, tzinfo=UTC), + departure_date=datetime( + 2016, 12, 5, 11, tzinfo=UTC), + arrival_date=datetime( + 2016, 12, 5, 12, tzinfo=UTC), mode_of_travel=ModeOfTravel.PLANE, dsa_region=dsa_lan) itinerary_item_2.airlines.all().delete() @@ -361,23 +380,29 @@ def test_travel_admin_export(self): itinerary_item_3 = ItineraryItemFactory(travel=travel_1, origin='Origin3', destination='Origin1', - departure_date=datetime(2016, 12, 6, 11, tzinfo=UTC), - arrival_date=datetime(2016, 12, 6, 12, tzinfo=UTC), + departure_date=datetime( + 2016, 12, 6, 11, tzinfo=UTC), + arrival_date=datetime( + 2016, 12, 6, 12, tzinfo=UTC), mode_of_travel=ModeOfTravel.PLANE, dsa_region=None) itinerary_item_3.airlines.all().delete() itinerary_item_3.airlines.add(airline_spiceair) # Second travel setup - another_traveler = UserFactory(first_name='Max', last_name='Mustermann') - travel_2 = TravelFactory(traveler=another_traveler, supervisor=self.unicef_staff) + another_traveler = UserFactory( + first_name='Max', last_name='Mustermann') + travel_2 = TravelFactory( + traveler=another_traveler, supervisor=self.unicef_staff) travel_2.itinerary.all().delete() itinerary_item_4 = ItineraryItemFactory(travel=travel_2, origin='Origin2', destination='Origin1', - departure_date=datetime(2016, 12, 5, 11, tzinfo=UTC), - arrival_date=datetime(2016, 12, 5, 12, tzinfo=UTC), + departure_date=datetime( + 2016, 12, 5, 11, tzinfo=UTC), + arrival_date=datetime( + 2016, 12, 5, 12, tzinfo=UTC), mode_of_travel=ModeOfTravel.PLANE, dsa_region=dsa_lan) itinerary_item_4.airlines.all().delete() @@ -386,8 +411,10 @@ def test_travel_admin_export(self): itinerary_item_5 = ItineraryItemFactory(travel=travel_2, origin='Origin3', destination='Origin1', - departure_date=datetime(2016, 12, 6, 11, tzinfo=UTC), - arrival_date=datetime(2016, 12, 6, 12, tzinfo=UTC), + departure_date=datetime( + 2016, 12, 6, 11, tzinfo=UTC), + arrival_date=datetime( + 2016, 12, 6, 12, tzinfo=UTC), mode_of_travel=ModeOfTravel.CAR, dsa_region=None) itinerary_item_5.airlines.all().delete() @@ -512,8 +539,10 @@ def test_invoice_export(self): usd = PublicsCurrencyFactory(name='USD', code='usd') # Setting up test data - travel_1 = TravelFactory(traveler=self.traveler, supervisor=self.unicef_staff) - travel_2 = TravelFactory(traveler=self.traveler, supervisor=self.unicef_staff) + travel_1 = TravelFactory( + traveler=self.traveler, supervisor=self.unicef_staff) + travel_2 = TravelFactory( + traveler=self.traveler, supervisor=self.unicef_staff) # Successful invoice invoice_1 = InvoiceFactory(travel=travel_1, diff --git a/src/etools/applications/t2f/views/exports.py b/src/etools/applications/t2f/views/exports.py index 705f598cc6..70562ad699 100644 --- a/src/etools/applications/t2f/views/exports.py +++ b/src/etools/applications/t2f/views/exports.py @@ -58,7 +58,7 @@ def __init__(self, travel, activity): self.activity = activity def get_queryset(self): - queryset = TravelActivity.objects.prefetch_related('travels', 'travels__traveler', 'travels__office', + queryset = TravelActivity.objects.prefetch_related('travels', 'travels__traveler', 'travels__office','travels__supervisor', 'travels__section', 'locations') queryset = queryset.select_related('partner', 'partnership', 'result', 'primary_traveler') queryset = queryset.order_by('id') From bc4de5911c514c01971e486b73a4ff6fca4fca39 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 14 Sep 2018 09:37:21 -0400 Subject: [PATCH 26/42] space lint --- src/etools/applications/t2f/views/exports.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/etools/applications/t2f/views/exports.py b/src/etools/applications/t2f/views/exports.py index 70562ad699..8ee3f42ff4 100644 --- a/src/etools/applications/t2f/views/exports.py +++ b/src/etools/applications/t2f/views/exports.py @@ -58,9 +58,10 @@ def __init__(self, travel, activity): self.activity = activity def get_queryset(self): - queryset = TravelActivity.objects.prefetch_related('travels', 'travels__traveler', 'travels__office','travels__supervisor', + queryset = TravelActivity.objects.prefetch_related('travels', 'travels__traveler', 'travels__office', 'travels__supervisor', 'travels__section', 'locations') - queryset = queryset.select_related('partner', 'partnership', 'result', 'primary_traveler') + queryset = queryset.select_related( + 'partner', 'partnership', 'result', 'primary_traveler') queryset = queryset.order_by('id') queries = [] @@ -92,7 +93,8 @@ class FinanceExport(ExportBaseView): def get(self, request): queryset = self.filter_queryset(self.get_queryset()) - queryset = queryset.select_related('traveler', 'office', 'section', 'supervisor') + queryset = queryset.select_related( + 'traveler', 'office', 'section', 'supervisor') serializer = self.get_serializer(queryset, many=True) response = Response(data=serializer.data, status=status.HTTP_200_OK) @@ -105,7 +107,8 @@ class TravelAdminExport(ExportBaseView): def get(self, request): travel_queryset = self.filter_queryset(self.get_queryset()) - queryset = ItineraryItem.objects.filter(travel__in=travel_queryset).order_by('travel__reference_number', 'id') + queryset = ItineraryItem.objects.filter( + travel__in=travel_queryset).order_by('travel__reference_number', 'id') queryset = queryset.select_related('travel', 'travel__office', 'travel__section', 'travel__traveler', 'dsa_region') serializer = self.get_serializer(queryset, many=True) @@ -120,9 +123,11 @@ class InvoiceExport(ExportBaseView): def get(self, request): travel_queryset = self.filter_queryset(self.get_queryset()) - queryset = InvoiceItem.objects.filter(invoice__travel__in=travel_queryset) + queryset = InvoiceItem.objects.filter( + invoice__travel__in=travel_queryset) queryset = queryset.order_by('invoice__travel__reference_number', 'id') - queryset = queryset.select_related('invoice', 'invoice__travel', 'invoice__currency', 'wbs', 'grant', 'fund') + queryset = queryset.select_related( + 'invoice', 'invoice__travel', 'invoice__currency', 'wbs', 'grant', 'fund') serializer = self.get_serializer(queryset, many=True) From 1196317ec314cba15bb90ac4107d89dcfd788f5c Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 11 Sep 2018 11:38:33 -0400 Subject: [PATCH 27/42] ip dashboard --- .../applications/action_points/models.py | 9 +- .../applications/partners/exports_v2.py | 26 +++++ .../serializers/partner_organization_v2.py | 21 +++- .../applications/partners/tests/test_views.py | 69 +++++++++++++ src/etools/applications/partners/urls_v2.py | 70 +++++++------ .../partners/views/partner_organization_v2.py | 99 ++++++++++++++++++- 6 files changed, 256 insertions(+), 38 deletions(-) diff --git a/src/etools/applications/action_points/models.py b/src/etools/applications/action_points/models.py index 3ec671eb17..fd86b00e19 100644 --- a/src/etools/applications/action_points/models.py +++ b/src/etools/applications/action_points/models.py @@ -13,8 +13,8 @@ from etools.applications.action_points.categories.models import Category from etools.applications.action_points.transitions.conditions import ActionPointCompleteActionsTakenCheck -from etools.applications.EquiTrack.utils import get_environment from etools.applications.action_points.transitions.serializers.serializers import ActionPointCompleteSerializer +from etools.applications.EquiTrack.utils import get_environment from etools.applications.permissions2.fsm import has_action_permission from etools.applications.utils.common.urlresolvers import build_frontend_url from etools.applications.utils.groups.wrappers import GroupWrapper @@ -23,9 +23,12 @@ class ActionPoint(TimeStampedModel): MODULE_CHOICES = Category.MODULE_CHOICES + STATUS_OPEN = 'open' + STATUS_COMPLETED = 'completed' + STATUSES = Choices( - ('open', _('Open')), - ('completed', _('Completed')), + (STATUS_OPEN, _('Open')), + (STATUS_COMPLETED, _('Completed')), ) STATUSES_DATES = { diff --git a/src/etools/applications/partners/exports_v2.py b/src/etools/applications/partners/exports_v2.py index 22ea6f2e99..b0a2622c04 100644 --- a/src/etools/applications/partners/exports_v2.py +++ b/src/etools/applications/partners/exports_v2.py @@ -110,6 +110,32 @@ class PartnerOrganizationHactCsvRenderer(FriendlyCSVRenderer): } +class PartnerOrganizationDashboardCsvRenderer(FriendlyCSVRenderer): + header = [ + 'name', + 'sections', + 'locations', + 'action_points', + 'total_ct_cp', + 'total_ct_ytd', + 'days_last_pv', + 'alert_no_recent_pv', + 'alert_no_pv', + ] + + labels = { + 'name': 'Implementing Partner', + 'sections': 'Sections', + 'locations': 'Locations', + 'action_points': 'Action Points #', + 'total_ct_cp': '$ Cash in the Current CP Cycle', + 'total_ct_ytd': '$ Cash in the Current Year', + 'days_last_pv': 'Days Since Last PV', + 'alert_no_recent_pv': 'Alert: No Recent PV', + 'alert_no_pv': 'Alert: No PV', + } + + class PartnerOrganizationSimpleHactCsvRenderer(FriendlyCSVRenderer): header = [ diff --git a/src/etools/applications/partners/serializers/partner_organization_v2.py b/src/etools/applications/partners/serializers/partner_organization_v2.py index 409920e2dd..4c7f76d51e 100644 --- a/src/etools/applications/partners/serializers/partner_organization_v2.py +++ b/src/etools/applications/partners/serializers/partner_organization_v2.py @@ -17,8 +17,9 @@ PartnerOrganization, PartnerPlannedVisits, PartnerStaffMember, + PartnerType, PlannedEngagement, - PartnerType) +) from etools.applications.partners.serializers.interventions_v2 import InterventionListSerializer @@ -296,6 +297,24 @@ class Meta: fields = "__all__" +class PartnerOrganizationDashboardSerializer(serializers.ModelSerializer): + sections = serializers.ReadOnlyField(read_only=True) + locations = serializers.ReadOnlyField(read_only=True) + action_points = serializers.ReadOnlyField(read_only=True) + + class Meta: + model = PartnerOrganization + fields = ( + 'id', + 'name', + 'sections', + 'locations', + 'action_points', + 'total_ct_cp', + 'total_ct_ytd', + ) + + class PartnerOrganizationCreateUpdateSerializer(SnapshotModelSerializer): staff_members = PartnerStaffMemberNestedSerializer(many=True, read_only=True) diff --git a/src/etools/applications/partners/tests/test_views.py b/src/etools/applications/partners/tests/test_views.py index f34afc6e33..66d2147cb2 100644 --- a/src/etools/applications/partners/tests/test_views.py +++ b/src/etools/applications/partners/tests/test_views.py @@ -17,8 +17,11 @@ from rest_framework import status from rest_framework.exceptions import ErrorDetail from rest_framework.test import APIRequestFactory +from unicef_locations.tests.factories import LocationFactory from unicef_snapshot.models import Activity +from etools.applications.action_points.models import ActionPoint +from etools.applications.action_points.tests.factories import ActionPointFactory from etools.applications.attachments.tests.factories import AttachmentFileTypeFactory from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.EquiTrack.tests.mixins import URLAssertionMixin @@ -59,6 +62,8 @@ ResultTypeFactory, SectionFactory, ) +from etools.applications.t2f.models import Travel, TravelType +from etools.applications.t2f.tests.factories import TravelActivityFactory, TravelFactory from etools.applications.users.tests.factories import GroupFactory, OfficeFactory, UserFactory @@ -2234,3 +2239,67 @@ def setUp(self): data=self.intervention_data ) self.intervention_data = response.data + + +class TestPartnerOrganizationDashboardAPIView(BaseTenantTestCase): + + @classmethod + def setUpTestData(cls): + cls.sec1, cls.sec2, _ = SectionFactory.create_batch(3) + cls.loc1, cls.loc2, _ = LocationFactory.create_batch(3) + cls.partner = PartnerFactory( + name="New", + vendor_number='007', + total_ct_cy=1000.00, + total_ct_cp=789.00, + total_ct_ytd=123.00, + # outstanding_dct_amount_6_to_9_months_usd=69, + # outstanding_dct_amount_more_than_9_months_usd=90, + ) + + cls.unicef_staff = UserFactory(is_staff=True) + today = datetime.date.today() + agreement = AgreementFactory(partner=cls.partner, signed_by_unicef_date=today) + travel = TravelFactory(status=Travel.COMPLETED, traveler=cls.unicef_staff) + ta = TravelActivityFactory(travel_type=TravelType.PROGRAMME_MONITORING, + date=datetime.date.today() - datetime.timedelta(200), + travels=[travel, ], primary_traveler=cls.unicef_staff) + cls.intervention = InterventionFactory(agreement=agreement) + cls.intervention.sections.set([cls.sec1, cls.sec2]) + cls.intervention.flat_locations.set([cls.loc1, cls.loc2]) + cls.intervention.travel_activities.set([ta, ]) + cls.intervention.save() + cls.act = ActionPointFactory.create_batch(3, travel_activity=ta, intervention=cls.intervention, + status=ActionPoint.STATUS_OPEN) + + ActionPointFactory.create_batch(3, intervention=cls.intervention, status=ActionPoint.STATUS_OPEN) + par = PartnerFactory(name="Other", vendor_number='008', total_ct_cy=1000.00) + int = InterventionFactory(agreement=AgreementFactory(partner=par, signed_by_unicef_date=today)) + ActionPointFactory.create_batch(3, travel_activity=ta, intervention=int, status=ActionPoint.STATUS_OPEN) + + def setUp(self): + self.response = self.forced_auth_req('get', reverse("partners_api:partner-dashboard"), user=self.unicef_staff) + data = self.response.data + self.assertEqual(len(data), 2) + self.record = data[0] + + def test_queryset(self): + self.assertEqual(self.record['total_ct_cp'], '789.00') + self.assertEqual(self.record['total_ct_ytd'], '123.00') + # self.assertEqual(self.record['outstanding_dct_amount_6_to_9_months_usd'], '69.00') + # self.assertEqual(self.record['outstanding_dct_amount_more_than_9_months_usd'], '90.00') + + def test_sections(self): + self.assertEqual(self.record['sections'], '{}|{}'.format(self.sec1.name, self.sec2.name)) + + def test_locations(self): + self.assertEqual(self.record['locations'], '{}|{}'.format(self.loc1.name, self.loc2.name)) + + def test_action_points(self): + self.assertEqual(self.record['action_points'], 3) + + def test_no_recent_programmatic_visit(self): + self.assertEquals(self.record['last_pv_date'].date(), datetime.date.today() - datetime.timedelta(200)) + self.assertEquals(self.record['days_last_pv'], 200) + self.assertTrue(self.record['alert_no_recent_pv']) + self.assertFalse(self.record['alert_no_pv']) diff --git a/src/etools/applications/partners/urls_v2.py b/src/etools/applications/partners/urls_v2.py index 85fd237c95..67227797f1 100644 --- a/src/etools/applications/partners/urls_v2.py +++ b/src/etools/applications/partners/urls_v2.py @@ -2,47 +2,56 @@ from rest_framework.urlpatterns import format_suffix_patterns -from etools.applications.partners.views.agreements_v2 import (AgreementAmendmentDeleteView, - AgreementAmendmentListAPIView, AgreementDeleteView, - AgreementDetailAPIView, AgreementListAPIView,) +from etools.applications.partners.views.agreements_v2 import ( + AgreementAmendmentDeleteView, + AgreementAmendmentListAPIView, + AgreementDeleteView, + AgreementDetailAPIView, + AgreementListAPIView, +) from etools.applications.partners.views.dashboards import InterventionPartnershipDashView -from etools.applications.partners.views.interventions_v2 import (InterventionAmendmentDeleteView, - InterventionAmendmentListAPIView, - InterventionAttachmentDeleteView, - InterventionDeleteView, InterventionDetailAPIView, - InterventionIndicatorListAPIView, - InterventionIndicatorsListView, - InterventionIndicatorsUpdateView, - InterventionListAPIView, InterventionListDashView, - InterventionListMapView, - InterventionLocationListAPIView, - InterventionLowerResultListCreateView, - InterventionLowerResultUpdateView, - InterventionRamIndicatorsView, - InterventionReportingPeriodDetailView, - InterventionReportingPeriodListCreateView, - InterventionReportingRequirementView, - InterventionResultLinkListCreateView, - InterventionResultLinkUpdateView, - InterventionResultListAPIView, - InterventionSectionLocationLinkListAPIView) +from etools.applications.partners.views.interventions_v2 import ( + InterventionAmendmentDeleteView, + InterventionAmendmentListAPIView, + InterventionAttachmentDeleteView, + InterventionDeleteView, + InterventionDetailAPIView, + InterventionIndicatorListAPIView, + InterventionIndicatorsListView, + InterventionIndicatorsUpdateView, + InterventionListAPIView, + InterventionListDashView, + InterventionListMapView, + InterventionLocationListAPIView, + InterventionLowerResultListCreateView, + InterventionLowerResultUpdateView, + InterventionRamIndicatorsView, + InterventionReportingPeriodDetailView, + InterventionReportingPeriodListCreateView, + InterventionReportingRequirementView, + InterventionResultLinkListCreateView, + InterventionResultLinkUpdateView, + InterventionResultListAPIView, + InterventionSectionLocationLinkListAPIView, +) from etools.applications.partners.views.partner_organization_v2 import ( + PartnerNotAssuranceCompliant, + PartnerNotProgrammaticVisitCompliant, + PartnerNotSpotCheckCompliant, PartnerOrganizationAddView, PartnerOrganizationAssessmentDeleteView, PartnerOrganizationAssessmentListView, + PartnerOrganizationDashboardAPIView, PartnerOrganizationDeleteView, PartnerOrganizationDetailAPIView, PartnerOrganizationHactAPIView, PartnerOrganizationListAPIView, PartnerOrganizationSimpleHactAPIView, - PartnerStaffMemberListAPIVIew, - PlannedEngagementAPIView, - PartnerNotAssuranceCompliant, - PartnerNotSpotCheckCompliant, - PartnerNotProgrammaticVisitCompliant, PartnerPlannedVisitsDeleteView, - PartnerWithSpecialAuditCompleted, + PartnerStaffMemberListAPIVIew, PartnerWithScheduledAuditCompleted, + PartnerWithSpecialAuditCompleted, + PlannedEngagementAPIView, ) from etools.applications.partners.views.v1 import PCAPDFView from etools.applications.partners.views.v2 import PMPDropdownsListApiView, PMPStaticDropdownsListAPIView @@ -97,6 +106,9 @@ url(r'^partners/hact/simple/$', view=PartnerOrganizationSimpleHactAPIView.as_view(http_method_names=['get', ]), name='partner-hact-simple'), + url(r'^partners/dashboard/$', + view=PartnerOrganizationDashboardAPIView.as_view(http_method_names=['get', ]), + name='partner-dashboard'), url(r'^partners/engagements/$', view=PlannedEngagementAPIView.as_view(http_method_names=['get', ]), name='partner-engagements'), diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index f86fb69784..32d3c3ee7f 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -1,9 +1,9 @@ import functools import operator -from datetime import datetime +from datetime import date, datetime -from django.db import transaction -from django.db.models import Q +from django.db import models, transaction +from django.db.models import Case, DateTimeField, DurationField, ExpressionWrapper, F, Max, Q, When from django.shortcuts import get_object_or_404 from etools_validator.mixins import ValidatorViewMixin @@ -16,16 +16,19 @@ ListCreateAPIView, RetrieveUpdateDestroyAPIView, ) -from rest_framework.permissions import IsAdminUser +from rest_framework.permissions import IsAdminUser, IsAuthenticatedOrReadOnly from rest_framework.response import Response from rest_framework_csv import renderers as r from unicef_restlib.views import QueryStringFilterMixin +from etools.applications.action_points.models import ActionPoint from etools.applications.EquiTrack.mixins import ExportModelMixin from etools.applications.EquiTrack.renderers import CSVFlatRenderer +from etools.applications.EquiTrack.serializers import StringConcat from etools.applications.EquiTrack.utils import get_data_from_insight from etools.applications.partners.exports_v2 import ( PartnerOrganizationCSVRenderer, + PartnerOrganizationDashboardCsvRenderer, PartnerOrganizationHactCsvRenderer, PartnerOrganizationSimpleHactCsvRenderer, ) @@ -56,6 +59,7 @@ CoreValuesAssessmentSerializer, MinimalPartnerOrganizationListSerializer, PartnerOrganizationCreateUpdateSerializer, + PartnerOrganizationDashboardSerializer, PartnerOrganizationDetailSerializer, PartnerOrganizationHactSerializer, PartnerOrganizationListSerializer, @@ -66,7 +70,7 @@ PlannedEngagementSerializer, ) from etools.applications.partners.views.helpers import set_tenant_or_fail -from etools.applications.t2f.models import TravelActivity +from etools.applications.t2f.models import Travel, TravelActivity, TravelType from etools.applications.vision.adapters.partner import PartnerSynchronizer @@ -202,6 +206,91 @@ def update(self, request, *args, **kwargs): return Response(PartnerOrganizationDetailSerializer(instance).data) +class PartnerOrganizationDashboardAPIView(ExportModelMixin, ListAPIView): + """Returns a list of Implementing partners for the dashboard.""" + + permission_classes = (IsAuthenticatedOrReadOnly,) + serializer_class = PartnerOrganizationDashboardSerializer + base_filename = 'IP_dashboard' + renderer_classes = (r.JSONRenderer, PartnerOrganizationDashboardCsvRenderer) + + def base_queryset(self): + return PartnerOrganization.objects.active() + + def get_queryset(self, format=None): + qs = self.base_queryset().prefetch_related( + 'agreements__interventions__sections', + 'agreements__interventions__flat_locations', + ).annotate( + sections=StringConcat("agreements__interventions__sections__name", separator="|", distinct=True), + locations=StringConcat("agreements__interventions__flat_locations__name", separator="|", distinct=True), + ) + # on hold: Cost Centre + # on hold: Outstanding DCT >6 and >9 months + return qs + + def list(self, request, format=None): + """ + Checks for format query parameter + :returns: JSON or CSV file + """ + query_params = self.request.query_params + queryset = self.filter_queryset(self.get_queryset()) + serializer = self.get_serializer(queryset, many=True) + self.update_serializer_data(serializer) + + response = Response(serializer.data) + + if "format" in query_params.keys(): + if query_params.get("format") == 'csv': + filename = self.get_filename() + response['Content-Disposition'] = f"attachment;filename={filename}.csv" + return response + + def get_filename(self): + today = date.today().strftime("%Y-%b-%d") + return f'{self.base_filename}_as_of_{today}' + + def update_serializer_data(self, serializer): + self._add_programmatic_visits(serializer) + self._add_action_points(serializer) + + def _add_programmatic_visits(self, serializer): + qs = self.base_queryset().annotate( + last_pv_date=Max(Case(When( + agreements__interventions__travel_activities__travel_type=TravelType.PROGRAMME_MONITORING, + agreements__interventions__travel_activities__travels__traveler=F( + 'agreements__interventions__travel_activities__primary_traveler'), + agreements__interventions__travel_activities__travels__status=Travel.COMPLETED, + then=F('agreements__interventions__travel_activities__date')), output_field=DateTimeField()) + ), + days_since_last_pv=ExpressionWrapper(datetime.now() - F('last_pv_date'), output_field=DurationField())) + pv_dates = {} + for partner in qs: + pv_dates[partner.pk] = { + "last_pv_date": partner.last_pv_date, + "days_last_pv": partner.days_since_last_pv.days if partner.days_since_last_pv else None, + } + + for item in serializer.data: # Add last_pv_date + pk = item["id"] + item["last_pv_date"] = pv_dates[pk]["last_pv_date"] + item["days_last_pv"] = pv_dates[pk]["days_last_pv"] + item["alert_no_recent_pv"] = pv_dates[pk]["days_last_pv"] > 180 if pv_dates[pk]["days_last_pv"] else True + item["alert_no_pv"] = pv_dates[pk]["days_last_pv"] is None + + def _add_action_points(self, serializer): + qs = self.base_queryset().annotate( + action_points=models.Sum(models.Case(models.When( + actionpoint__travel_activity__travel_type__in=[TravelType.PROGRAMME_MONITORING, TravelType.SPOT_CHECK], + actionpoint__status=ActionPoint.STATUS_OPEN, then=1), + default=0, output_field=models.IntegerField(), distinct=True)), + ) + ap = {partner.pk: partner.action_points for partner in qs} + for item in serializer.data: + item['action_points'] = ap[item["id"]] + + class PartnerOrganizationHactAPIView(ListAPIView): """ From e429578f18bb2fa3c9698ab26d28c23fe5aacc2a Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 20 Aug 2018 20:03:15 -0400 Subject: [PATCH 28/42] moving sychronizers to related app --- .../purchase_order/synchronizers.py} | 2 +- .../audit/purchase_order/tasks.py | 32 ++++++++++ .../purchase_order/tests}/__init__.py | 0 .../tests/test_synchronizers.py} | 30 ++++++++-- .../audit/purchase_order/tests/test_tasks.py | 14 +++++ src/etools/applications/audit/views.py | 4 +- .../funding.py => funds/synchronizers.py} | 9 ++- .../tests/test_synchronizers.py} | 10 ++-- .../applications/management/views/general.py | 4 +- .../applications/management/views/reports.py | 2 +- .../partner.py => partners/synchronizers.py} | 7 ++- .../tests/test_synchronizers.py} | 6 +- .../partners/views/partner_organization_v2.py | 2 +- .../synchronizers.py} | 59 +++++++++++++++++- .../tests/test_synchronizers.py} | 10 ++-- .../programme.py => reports/synchronizers.py} | 1 - .../tests/test_synchronizers.py} | 10 ++-- .../tpmpartners/synchronizers.py} | 2 +- .../applications/tpm/tpmpartners/tasks.py | 33 ++++++++++ src/etools/applications/tpm/views.py | 2 +- src/etools/applications/users/admin.py | 8 +-- .../vision/adapters/workspaces.py | 50 ---------------- .../{adapters/manual.py => synchronizers.py} | 0 src/etools/applications/vision/tasks.py | 60 +------------------ .../vision/tests/test_adapter_manual.py | 25 -------- ..._synchronizer.py => test_synchronizers.py} | 22 +++++++ .../applications/vision/tests/test_tasks.py | 21 ------- 27 files changed, 227 insertions(+), 198 deletions(-) rename src/etools/applications/{vision/adapters/purchase_order.py => audit/purchase_order/synchronizers.py} (95%) create mode 100644 src/etools/applications/audit/purchase_order/tasks.py rename src/etools/applications/{vision/adapters => audit/purchase_order/tests}/__init__.py (100%) rename src/etools/applications/{vision/tests/test_adapter_purchase_order.py => audit/purchase_order/tests/test_synchronizers.py} (69%) create mode 100644 src/etools/applications/audit/purchase_order/tests/test_tasks.py rename src/etools/applications/{vision/adapters/funding.py => funds/synchronizers.py} (98%) rename src/etools/applications/{vision/tests/test_adapter_funding.py => funds/tests/test_synchronizers.py} (98%) rename src/etools/applications/{vision/adapters/partner.py => partners/synchronizers.py} (98%) rename src/etools/applications/{vision/tests/test_adapter_partner.py => partners/tests/test_synchronizers.py} (96%) rename src/etools/applications/{vision/adapters/publics_adapter.py => publics/synchronizers.py} (78%) rename src/etools/applications/{vision/tests/test_adapter_publics.py => publics/tests/test_synchronizers.py} (96%) rename src/etools/applications/{vision/adapters/programme.py => reports/synchronizers.py} (99%) rename src/etools/applications/{vision/tests/test_adapter_programme.py => reports/tests/test_synchronizers.py} (98%) rename src/etools/applications/{vision/adapters/tpm_adapter.py => tpm/tpmpartners/synchronizers.py} (96%) create mode 100644 src/etools/applications/tpm/tpmpartners/tasks.py delete mode 100644 src/etools/applications/vision/adapters/workspaces.py rename src/etools/applications/vision/{adapters/manual.py => synchronizers.py} (100%) delete mode 100644 src/etools/applications/vision/tests/test_adapter_manual.py rename src/etools/applications/vision/tests/{test_synchronizer.py => test_synchronizers.py} (96%) diff --git a/src/etools/applications/vision/adapters/purchase_order.py b/src/etools/applications/audit/purchase_order/synchronizers.py similarity index 95% rename from src/etools/applications/vision/adapters/purchase_order.py rename to src/etools/applications/audit/purchase_order/synchronizers.py index 840e9da416..d22d0c277c 100644 --- a/src/etools/applications/vision/adapters/purchase_order.py +++ b/src/etools/applications/audit/purchase_order/synchronizers.py @@ -3,7 +3,7 @@ from etools.applications.audit.purchase_order.models import AuditorFirm, PurchaseOrder, PurchaseOrderItem from etools.applications.funds.models import Donor, Grant -from etools.applications.vision.adapters.manual import ManualVisionSynchronizer +from etools.applications.vision.synchronizers import ManualVisionSynchronizer class POSynchronizer(ManualVisionSynchronizer): diff --git a/src/etools/applications/audit/purchase_order/tasks.py b/src/etools/applications/audit/purchase_order/tasks.py new file mode 100644 index 0000000000..d1d3ee589b --- /dev/null +++ b/src/etools/applications/audit/purchase_order/tasks.py @@ -0,0 +1,32 @@ +from celery.utils.log import get_task_logger + +# Not scheduled by any code in this repo, but by other means, so keep it around. +# Continues on to the next country on any VisionException, so no need to have +# celery retry it in that case. +from etools.applications.audit.purchase_order.synchronizers import POSynchronizer +from etools.applications.users.models import Country +from etools.applications.vision.exceptions import VisionException +from etools.config.celery import app + +logger = get_task_logger(__name__) + + +@app.task +def update_purchase_orders(country_name=None): + logger.info(u'Starting update values for purchase order') + countries = Country.objects.filter(vision_sync_enabled=True) + processed = [] + if country_name is not None: + countries = countries.filter(name=country_name) + for country in countries: + try: + logger.info(u'Starting purchase order update for country {}'.format( + country.name + )) + POSynchronizer(country).sync() + processed.append(country.name) + logger.info(u"Update finished successfully for {}".format(country.name)) + except VisionException: + logger.exception(u"{} sync failed".format(POSynchronizer.__name__)) + # Keep going to the next country + logger.info(u'Purchase orders synced successfully for {}.'.format(u', '.join(processed))) diff --git a/src/etools/applications/vision/adapters/__init__.py b/src/etools/applications/audit/purchase_order/tests/__init__.py similarity index 100% rename from src/etools/applications/vision/adapters/__init__.py rename to src/etools/applications/audit/purchase_order/tests/__init__.py diff --git a/src/etools/applications/vision/tests/test_adapter_purchase_order.py b/src/etools/applications/audit/purchase_order/tests/test_synchronizers.py similarity index 69% rename from src/etools/applications/vision/tests/test_adapter_purchase_order.py rename to src/etools/applications/audit/purchase_order/tests/test_synchronizers.py index a83cd6a7c7..ad03a8b1a5 100644 --- a/src/etools/applications/vision/tests/test_adapter_purchase_order.py +++ b/src/etools/applications/audit/purchase_order/tests/test_synchronizers.py @@ -1,11 +1,13 @@ import json +from unittest import mock +from etools.applications.audit.purchase_order import synchronizers from etools.applications.audit.purchase_order.models import AuditorFirm, PurchaseOrder, PurchaseOrderItem +from etools.applications.audit.purchase_order.tasks import update_purchase_orders from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.funds.models import Donor, Grant from etools.applications.users.models import Country -from etools.applications.vision.adapters import purchase_order as adapter class TestPSynchronizer(BaseTenantTestCase): @@ -25,14 +27,14 @@ def setUp(self): "GRANT_REF": "Grantor", "PO_ITEM": "456", } - self.adapter = adapter.POSynchronizer(self.country) + self.adapter = synchronizers.POSynchronizer(self.country) def test_init_no_object_number(self): - a = adapter.POSynchronizer(self.country) + a = synchronizers.POSynchronizer(self.country) self.assertEqual(a.country, self.country) def test_init(self): - a = adapter.POSynchronizer(self.country, object_number="123") + a = synchronizers.POSynchronizer(self.country, object_number="123") self.assertEqual(a.country, self.country) def test_convert_records_list(self): @@ -78,3 +80,23 @@ def test_save_records(self): self.assertTrue(auditor_qs.exists()) self.assertTrue(donor_qs.exists()) self.assertTrue(grant_qs.exists()) + + +class TestUpdatePurchaseOrders(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.country = Country.objects.first() + + @mock.patch("etools.applications.vision.tasks.logger.exception") + def test_update_purchase_orders_no_country(self, mock_logger_exception): + """Ensure no exceptions if no countries""" + update_purchase_orders(country_name="Wrong") + self.assertEqual(mock_logger_exception.call_count, 0) + + @mock.patch("etools.applications.vision.tasks.logger.exception") + @mock.patch("etools.applications.audit.purchase_order.tasks.POSynchronizer") + def test_update_purchase_orders(self, synchronizer, mock_logger_exception): + """Ensure no exceptions if no countries""" + update_purchase_orders() + self.assertEqual(mock_logger_exception.call_count, 0) + self.assertEqual(synchronizer.call_count, 1) diff --git a/src/etools/applications/audit/purchase_order/tests/test_tasks.py b/src/etools/applications/audit/purchase_order/tests/test_tasks.py new file mode 100644 index 0000000000..5c525fa8f2 --- /dev/null +++ b/src/etools/applications/audit/purchase_order/tests/test_tasks.py @@ -0,0 +1,14 @@ +from unittest.mock import patch + +from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase +from etools.applications.audit.purchase_order.tasks import update_purchase_orders + + +class TestUpdatePurchaseOrders(BaseTenantTestCase): + + @patch("etools.applications.audit.purchase_order.synchronizers.POSynchronizer.sync") + @patch('etools.applications.audit.purchase_order.tasks.logger', spec=['info', 'error']) + def test_update_purchase_orders(self, logger, mock_send): + update_purchase_orders() + self.assertEqual(mock_send.call_count, 1) + self.assertEqual(logger.info.call_count, 4) diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index d9fcf57b22..037e9fdf50 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -49,6 +49,7 @@ UNICEFUser, ) from etools.applications.audit.purchase_order.models import AuditorFirm, AuditorStaffMember, PurchaseOrder +from etools.applications.audit.purchase_order.synchronizers import POSynchronizer from etools.applications.audit.serializers.auditor import ( AuditorFirmLightSerializer, AuditorFirmSerializer, @@ -68,8 +69,8 @@ ReportAttachmentSerializer, SpecialAuditSerializer, SpotCheckSerializer, - StaffSpotCheckSerializer, StaffSpotCheckListSerializer, + StaffSpotCheckSerializer, ) from etools.applications.audit.serializers.export import ( AuditDetailCSVSerializer, @@ -86,7 +87,6 @@ from etools.applications.permissions2.conditions import ObjectStatusCondition from etools.applications.permissions2.drf_permissions import get_permission_for_targets, NestedPermission from etools.applications.permissions2.views import PermittedFSMActionMixin, PermittedSerializerMixin -from etools.applications.vision.adapters.purchase_order import POSynchronizer class BaseAuditViewSet( diff --git a/src/etools/applications/vision/adapters/funding.py b/src/etools/applications/funds/synchronizers.py similarity index 98% rename from src/etools/applications/vision/adapters/funding.py rename to src/etools/applications/funds/synchronizers.py index 5e8dd45ca3..9aed75f936 100644 --- a/src/etools/applications/vision/adapters/funding.py +++ b/src/etools/applications/funds/synchronizers.py @@ -5,9 +5,12 @@ from django.db.models import Sum - -from etools.applications.funds.models import (FundsCommitmentHeader, FundsCommitmentItem, - FundsReservationHeader, FundsReservationItem,) +from etools.applications.funds.models import ( + FundsCommitmentHeader, + FundsCommitmentItem, + FundsReservationHeader, + FundsReservationItem, +) from etools.applications.vision.utils import comp_decimals from etools.applications.vision.vision_data_synchronizer import FileDataSynchronizer, VisionDataSynchronizer diff --git a/src/etools/applications/vision/tests/test_adapter_funding.py b/src/etools/applications/funds/tests/test_synchronizers.py similarity index 98% rename from src/etools/applications/vision/tests/test_adapter_funding.py rename to src/etools/applications/funds/tests/test_synchronizers.py index f44a4790da..0066a39b69 100644 --- a/src/etools/applications/vision/tests/test_adapter_funding.py +++ b/src/etools/applications/funds/tests/test_synchronizers.py @@ -9,7 +9,7 @@ from etools.applications.funds.tests.factories import (FundsCommitmentHeaderFactory, FundsCommitmentItemFactory, FundsReservationHeaderFactory, FundsReservationItemFactory,) from etools.applications.users.models import Country -from etools.applications.vision.adapters import funding as adapter +from etools.applications.funds import synchronizers class TestFundReservationsSynchronizer(BaseTenantTestCase): @@ -102,10 +102,10 @@ def setUp(self): start_date=datetime.date(2015, 1, 13), end_date=datetime.date(2015, 12, 20), ) - self.adapter = adapter.FundReservationsSynchronizer(self.country) + self.adapter = synchronizers.FundReservationsSynchronizer(self.country) def test_init(self): - a = adapter.FundReservationsSynchronizer(self.country) + a = synchronizers.FundReservationsSynchronizer(self.country) self.assertEqual(a.header_records, {}) self.assertEqual(a.item_records, {}) self.assertEqual(a.fr_headers, {}) @@ -366,10 +366,10 @@ def setUp(self): exchange_rate=self.data["EXCHANGE_RATE"], responsible_person=self.data["RESP_PERSON"], ) - self.adapter = adapter.FundCommitmentSynchronizer(self.country) + self.adapter = synchronizers.FundCommitmentSynchronizer(self.country) def test_init(self): - a = adapter.FundCommitmentSynchronizer(self.country) + a = synchronizers.FundCommitmentSynchronizer(self.country) self.assertEqual(a.header_records, {}) self.assertEqual(a.item_records, {}) self.assertEqual(a.fc_headers, {}) diff --git a/src/etools/applications/management/views/general.py b/src/etools/applications/management/views/general.py index 551d0e8b65..8b752a3bc0 100644 --- a/src/etools/applications/management/views/general.py +++ b/src/etools/applications/management/views/general.py @@ -4,9 +4,9 @@ from rest_framework.views import APIView from unicef_restlib.permissions import IsSuperUser +from etools.applications.funds.synchronizers import FundReservationsSynchronizer +from etools.applications.publics.synchronizers import CountryLongNameSync from etools.applications.users.models import Country -from etools.applications.vision.adapters.funding import FundReservationsSynchronizer -from etools.applications.vision.adapters.workspaces import CountryLongNameSync class InvalidateCache(APIView): diff --git a/src/etools/applications/management/views/reports.py b/src/etools/applications/management/views/reports.py index e874f2b10f..b919fc4878 100644 --- a/src/etools/applications/management/views/reports.py +++ b/src/etools/applications/management/views/reports.py @@ -5,8 +5,8 @@ from unicef_restlib.permissions import IsSuperUser from etools.applications.EquiTrack.utils import set_country +from etools.applications.reports.synchronizers import ProgrammeSynchronizer from etools.applications.users.models import Country as Workspace -from etools.applications.vision.adapters.programme import ProgrammeSynchronizer class LoadResultStructure(APIView): diff --git a/src/etools/applications/vision/adapters/partner.py b/src/etools/applications/partners/synchronizers.py similarity index 98% rename from src/etools/applications/vision/adapters/partner.py rename to src/etools/applications/partners/synchronizers.py index 217b332bc5..436b9f7657 100644 --- a/src/etools/applications/vision/adapters/partner.py +++ b/src/etools/applications/partners/synchronizers.py @@ -9,7 +9,10 @@ from etools.applications.partners.tasks import notify_partner_hidden from etools.applications.vision.utils import comp_decimals from etools.applications.vision.vision_data_synchronizer import ( - FileDataSynchronizer, VISION_NO_DATA_MESSAGE, VisionDataSynchronizer,) + FileDataSynchronizer, + VISION_NO_DATA_MESSAGE, + VisionDataSynchronizer, +) logger = logging.getLogger(__name__) @@ -283,7 +286,7 @@ def get_type_of_assessment(partner): class FilePartnerSynchronizer(FileDataSynchronizer, PartnerSynchronizer): """ - >>> from etools.applications.vision.adapters.partner import FilePartnerSynchronizer + >>> from etools.applications.partners.synchronizers import FilePartnerSynchronizer >>> from etools.applications.users.models import Country >>> country = Country.objects.get(name='Indonesia') >>> filename = '/home/user/Downloads/partners.json' diff --git a/src/etools/applications/vision/tests/test_adapter_partner.py b/src/etools/applications/partners/tests/test_synchronizers.py similarity index 96% rename from src/etools/applications/vision/tests/test_adapter_partner.py rename to src/etools/applications/partners/tests/test_synchronizers.py index 7ccb49b0f5..675212b11d 100644 --- a/src/etools/applications/vision/tests/test_adapter_partner.py +++ b/src/etools/applications/partners/tests/test_synchronizers.py @@ -6,7 +6,7 @@ from etools.applications.partners.models import PartnerOrganization from etools.applications.partners.tests.factories import PartnerFactory from etools.applications.users.models import Country -from etools.applications.vision.adapters import partner as adapter +from etools.applications.partners import synchronizers class TestPartnerSynchronizer(BaseTenantTestCase): @@ -27,7 +27,7 @@ def setUp(self): "TOTAL_CASH_TRANSFERRED_YTD": "70.00", } self.records = {"ROWSET": {"ROW": [self.data]}} - self.adapter = adapter.PartnerSynchronizer(self.country) + self.adapter = synchronizers.PartnerSynchronizer(self.country) def test_convert_records(self): self.assertEqual( @@ -51,7 +51,7 @@ def test_get_json(self): self.assertEqual(response, self.data) def test_get_json_no_data(self): - response = self.adapter._get_json(adapter.VISION_NO_DATA_MESSAGE) + response = self.adapter._get_json(synchronizers.VISION_NO_DATA_MESSAGE) self.assertEqual(response, []) def test_get_cso_type_none(self): diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index f86fb69784..f92c69faaf 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -65,9 +65,9 @@ PlannedEngagementNestedSerializer, PlannedEngagementSerializer, ) +from etools.applications.partners.synchronizers import PartnerSynchronizer from etools.applications.partners.views.helpers import set_tenant_or_fail from etools.applications.t2f.models import TravelActivity -from etools.applications.vision.adapters.partner import PartnerSynchronizer class PartnerOrganizationListAPIView(QueryStringFilterMixin, ExportModelMixin, ListCreateAPIView): diff --git a/src/etools/applications/vision/adapters/publics_adapter.py b/src/etools/applications/publics/synchronizers.py similarity index 78% rename from src/etools/applications/vision/adapters/publics_adapter.py rename to src/etools/applications/publics/synchronizers.py index 92aaf84e67..4a24f51fdf 100644 --- a/src/etools/applications/vision/adapters/publics_adapter.py +++ b/src/etools/applications/publics/synchronizers.py @@ -5,8 +5,17 @@ from django.core.exceptions import ObjectDoesNotExist -from etools.applications.publics.models import (BusinessArea, Country, Currency, ExchangeRate, - Fund, Grant, TravelAgent, TravelExpenseType, WBS,) +from etools.applications.publics.models import ( + BusinessArea, + Country, + Currency, + ExchangeRate, + Fund, + Grant, + TravelAgent, + TravelExpenseType, + WBS, +) from etools.applications.publics.views import WBSGrantFundView from etools.applications.vision.vision_data_synchronizer import VisionDataSynchronizer @@ -207,3 +216,49 @@ def _save_records(self, records): log.info('Travel agent %s saved.', travel_agent.name) return processed + + +class CountryLongNameSync(VisionDataSynchronizer): + ENDPOINT = 'GetBusinessAreaList_JSON' + GLOBAL_CALL = True + + def __init__(self, *args, **kwargs): + self.countries_qs = Country.objects.exclude(business_area_code='0') + super(CountryLongNameSync, self).__init__(*args, **kwargs) + + def _convert_records(self, records): + records = json.loads(records['GetBusinessAreaList_JSONResult']) + records = dict((r['BUSINESS_AREA_CODE'], r) for r in records) + return records + + def _save_records(self, records): + countries = self.countries_qs.all() + countries_updated = [] + for c in countries: + try: + new_name = records[c.business_area_code]['BUSINESS_AREA_LONG_NAME'] + except KeyError: + continue + if c.long_name != new_name: + countries_updated.append((c.business_area_code, c.name, c.long_name, new_name)) + c.long_name = new_name + c.save() + + return { + 'details': '\n'.join(['Business Area: {} - {}, Old Long Name: {}, New Name: {}'.format(*c) + for c in countries_updated]), + 'total_records': len(records), # len(records), + 'processed': len(list(countries_updated)) + } + + def _filter_records(self, records): + local_business_area_codes = self.countries_qs.values_list('business_area_codes', flat=True) + + records = super()._filter_records(records) + + def bad_record(record): + if not record['BUSINESS_AREA_CODE'] in local_business_area_codes: + return False + return True + + return [rec for rec in records if bad_record(rec)] diff --git a/src/etools/applications/vision/tests/test_adapter_publics.py b/src/etools/applications/publics/tests/test_synchronizers.py similarity index 96% rename from src/etools/applications/vision/tests/test_adapter_publics.py rename to src/etools/applications/publics/tests/test_synchronizers.py index 0041677b38..56a46aaf66 100644 --- a/src/etools/applications/vision/tests/test_adapter_publics.py +++ b/src/etools/applications/publics/tests/test_synchronizers.py @@ -7,7 +7,7 @@ from etools.applications.publics.tests.factories import (PublicsBusinessAreaFactory, PublicsCountryFactory, PublicsFundFactory, PublicsGrantFactory, TravelAgentFactory,) from etools.applications.users.models import Country -from etools.applications.vision.adapters import publics_adapter as adapter +from etools.applications.publics import synchronizers class TestCostAssignmentSynch(BaseTenantTestCase): @@ -31,10 +31,10 @@ def setUp(self): {"grant_name": "123", "fund_type": "321"} ] } - self.adapter = adapter.CostAssignmentSynch(self.country) + self.adapter = synchronizers.CostAssignmentSynch(self.country) def test_init(self): - a = adapter.CostAssignmentSynch(self.country) + a = synchronizers.CostAssignmentSynch(self.country) self.assertEqual(len(a.grants.keys()), Grant.objects.count()) self.assertEqual(len(a.funds.keys()), Fund.objects.count()) self.assertIsNone(a.business_area) @@ -146,7 +146,7 @@ def setUp(self): "VALID_FROM": "1-Jan-16", "VALID_TO": "31-Dec-17", } - self.adapter = adapter.CurrencySynchronizer(self.country) + self.adapter = synchronizers.CurrencySynchronizer(self.country) def test_convert_records(self): self.assertEqual( @@ -176,7 +176,7 @@ def setUp(self): "VENDOR_CITY": "New York", "VENDOR_CTRY_CODE": "USD", } - self.adapter = adapter.TravelAgenciesSynchronizer(self.country) + self.adapter = synchronizers.TravelAgenciesSynchronizer(self.country) def test_convert_records(self): self.assertEqual( diff --git a/src/etools/applications/vision/adapters/programme.py b/src/etools/applications/reports/synchronizers.py similarity index 99% rename from src/etools/applications/vision/adapters/programme.py rename to src/etools/applications/reports/synchronizers.py index d290c364ba..0256b2f996 100644 --- a/src/etools/applications/vision/adapters/programme.py +++ b/src/etools/applications/reports/synchronizers.py @@ -5,7 +5,6 @@ from django.db import transaction - from etools.applications.reports.models import CountryProgramme, Indicator, Result, ResultType from etools.applications.vision.utils import wcf_json_date_as_date from etools.applications.vision.vision_data_synchronizer import VISION_NO_DATA_MESSAGE, VisionDataSynchronizer diff --git a/src/etools/applications/vision/tests/test_adapter_programme.py b/src/etools/applications/reports/tests/test_synchronizers.py similarity index 98% rename from src/etools/applications/vision/tests/test_adapter_programme.py rename to src/etools/applications/reports/tests/test_synchronizers.py index b248adbebf..016674319f 100644 --- a/src/etools/applications/vision/tests/test_adapter_programme.py +++ b/src/etools/applications/reports/tests/test_synchronizers.py @@ -7,7 +7,7 @@ from etools.applications.reports.tests.factories import (CountryProgrammeFactory, IndicatorFactory, ResultFactory, ResultTypeFactory,) from etools.applications.users.models import Country -from etools.applications.vision.adapters import programme as adapter +from etools.applications.reports import synchronizers class TestResultStructureSynchronizer(BaseTenantTestCase): @@ -19,7 +19,7 @@ def setUpTestData(cls): def setUp(self): self.data = {"test": "123"} - self.adapter = adapter.ResultStructureSynchronizer(self.data) + self.adapter = synchronizers.ResultStructureSynchronizer(self.data) def test_init(self): self.assertEqual(self.adapter.data, self.data) @@ -380,13 +380,13 @@ def setUp(self): "PROGRAMME_AREA_CODE": "", "PROGRAMME_AREA_NAME": "", } - self.adapter = adapter.ProgrammeSynchronizer(self.country) + self.adapter = synchronizers.ProgrammeSynchronizer(self.country) def test_get_json(self): data = {"test": "123"} self.assertEqual(self.adapter._get_json(data), data) self.assertEqual( - self.adapter._get_json(adapter.VISION_NO_DATA_MESSAGE), + self.adapter._get_json(synchronizers.VISION_NO_DATA_MESSAGE), [] ) @@ -581,7 +581,7 @@ def setUp(self): "BASELINE": "BLINE", "TARGET": "Target", } - self.adapter = adapter.RAMSynchronizer(self.country) + self.adapter = synchronizers.RAMSynchronizer(self.country) def test_convert_records(self): records = json.dumps([self.data]) diff --git a/src/etools/applications/vision/adapters/tpm_adapter.py b/src/etools/applications/tpm/tpmpartners/synchronizers.py similarity index 96% rename from src/etools/applications/vision/adapters/tpm_adapter.py rename to src/etools/applications/tpm/tpmpartners/synchronizers.py index e19501d220..d95c40dfbf 100644 --- a/src/etools/applications/vision/adapters/tpm_adapter.py +++ b/src/etools/applications/tpm/tpmpartners/synchronizers.py @@ -3,7 +3,7 @@ from etools.applications.publics.models import Country from etools.applications.tpm.tpmpartners.models import TPMPartner -from etools.applications.vision.adapters.manual import ManualVisionSynchronizer +from etools.applications.vision.synchronizers import ManualVisionSynchronizer def _get_country_name(value): diff --git a/src/etools/applications/tpm/tpmpartners/tasks.py b/src/etools/applications/tpm/tpmpartners/tasks.py new file mode 100644 index 0000000000..635393827c --- /dev/null +++ b/src/etools/applications/tpm/tpmpartners/tasks.py @@ -0,0 +1,33 @@ +from celery.utils.log import get_task_logger + +from etools.applications.tpm.tpmpartners.models import TPMPartner +from etools.applications.tpm.tpmpartners.synchronizers import TPMPartnerSynchronizer +from etools.applications.users.models import Country +from etools.applications.vision.exceptions import VisionException +from etools.config.celery import app + +logger = get_task_logger(__name__) + + +@app.task +def update_tpm_partners(country_name=None): + logger.info(u'Starting update values for TPM partners') + countries = Country.objects.filter(vision_sync_enabled=True) + processed = [] + if country_name is not None: + countries = countries.filter(name=country_name) + for country in countries: + try: + logger.info(u'Starting TPM partners update for country {}'.format( + country.name + )) + for partner in TPMPartner.objects.all(): + TPMPartnerSynchronizer( + country=country, + object_number=partner.vendor_number + ).sync() + processed.append(country.name) + logger.info(u"Update finished successfully for {}".format(country.name)) + except VisionException: + logger.exception(u"{} sync failed".format(TPMPartnerSynchronizer.__name__)) + logger.info(u'TPM Partners synced successfully for {}.'.format(u', '.join(processed))) diff --git a/src/etools/applications/tpm/views.py b/src/etools/applications/tpm/views.py index 444efbab65..fb3e94b33b 100644 --- a/src/etools/applications/tpm/views.py +++ b/src/etools/applications/tpm/views.py @@ -72,7 +72,7 @@ TPMVisitSerializer, ) from etools.applications.tpm.tpmpartners.models import TPMPartner, TPMPartnerStaffMember -from etools.applications.vision.adapters.tpm_adapter import TPMPartnerManualSynchronizer +from etools.applications.tpm.tpmpartners.synchronizers import TPMPartnerManualSynchronizer class BaseTPMViewSet( diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index 9205a6aa4c..320f585e4e 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -7,15 +7,11 @@ from django.http.response import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse + from tenant_schemas.utils import get_public_schema_name from etools.applications.hact.tasks import update_hact_for_country, update_hact_values -from etools.applications.users.models import ( - Country, - Office, - UserProfile, - WorkspaceCounter, -) +from etools.applications.users.models import Country, Office, UserProfile, WorkspaceCounter from etools.applications.vision.tasks import sync_handler, vision_sync_task from etools.libraries.azure_graph_api.tasks import sync_user diff --git a/src/etools/applications/vision/adapters/workspaces.py b/src/etools/applications/vision/adapters/workspaces.py deleted file mode 100644 index 56def07a7a..0000000000 --- a/src/etools/applications/vision/adapters/workspaces.py +++ /dev/null @@ -1,50 +0,0 @@ -import json - -from etools.applications.users.models import Country -from etools.applications.vision.vision_data_synchronizer import VisionDataSynchronizer - - -class CountryLongNameSync(VisionDataSynchronizer): - ENDPOINT = 'GetBusinessAreaList_JSON' - GLOBAL_CALL = True - - def __init__(self, *args, **kwargs): - self.countries_qs = Country.objects.exclude(business_area_code='0') - super(CountryLongNameSync, self).__init__(*args, **kwargs) - - def _convert_records(self, records): - records = json.loads(records['GetBusinessAreaList_JSONResult']) - records = dict((r['BUSINESS_AREA_CODE'], r) for r in records) - return records - - def _save_records(self, records): - countries = self.countries_qs.all() - countries_updated = [] - for c in countries: - try: - new_name = records[c.business_area_code]['BUSINESS_AREA_LONG_NAME'] - except KeyError: - continue - if c.long_name != new_name: - countries_updated.append((c.business_area_code, c.name, c.long_name, new_name)) - c.long_name = new_name - c.save() - - return { - 'details': '\n'.join(['Business Area: {} - {}, Old Long Name: {}, New Name: {}'.format(*c) - for c in countries_updated]), - 'total_records': len(records), # len(records), - 'processed': len(list(countries_updated)) - } - - def _filter_records(self, records): - local_business_area_codes = self.countries_qs.values_list('business_area_codes', flat=True) - - records = super()._filter_records(records) - - def bad_record(record): - if not record['BUSINESS_AREA_CODE'] in local_business_area_codes: - return False - return True - - return [rec for rec in records if bad_record(rec)] diff --git a/src/etools/applications/vision/adapters/manual.py b/src/etools/applications/vision/synchronizers.py similarity index 100% rename from src/etools/applications/vision/adapters/manual.py rename to src/etools/applications/vision/synchronizers.py diff --git a/src/etools/applications/vision/tasks.py b/src/etools/applications/vision/tasks.py index aac2d7c49d..2dbf525322 100644 --- a/src/etools/applications/vision/tasks.py +++ b/src/etools/applications/vision/tasks.py @@ -3,13 +3,10 @@ from celery.utils.log import get_task_logger -from etools.applications.tpm.tpmpartners.models import TPMPartner +from etools.applications.funds.synchronizers import FundReservationsSynchronizer, FundCommitmentSynchronizer +from etools.applications.partners.synchronizers import PartnerSynchronizer +from etools.applications.reports.synchronizers import ProgrammeSynchronizer, RAMSynchronizer from etools.applications.users.models import Country -from etools.applications.vision.adapters.funding import FundCommitmentSynchronizer, FundReservationsSynchronizer -from etools.applications.vision.adapters.partner import PartnerSynchronizer -from etools.applications.vision.adapters.programme import ProgrammeSynchronizer, RAMSynchronizer -from etools.applications.vision.adapters.purchase_order import POSynchronizer -from etools.applications.vision.adapters.tpm_adapter import TPMPartnerSynchronizer from etools.applications.vision.exceptions import VisionException from etools.config.celery import app, send_to_slack @@ -93,54 +90,3 @@ def sync_handler(self, country_name, handler): # The 'autoretry_for' in the task decorator tells Celery to # retry this a few times on VisionExceptions, so just re-raise it raise - - -# Not scheduled by any code in this repo, but by other means, so keep it around. -# Continues on to the next country on any VisionException, so no need to have -# celery retry it in that case. -# TODO: Write some tests for it! -@app.task -def update_purchase_orders(country_name=None): - logger.info(u'Starting update values for purchase order') - countries = Country.objects.filter(vision_sync_enabled=True) - processed = [] - if country_name is not None: - countries = countries.filter(name=country_name) - for country in countries: - connection.set_tenant(country) - try: - logger.info(u'Starting purchase order update for country {}'.format( - country.name - )) - POSynchronizer(country).sync() - processed.append(country.name) - logger.info(u"Update finished successfully for {}".format(country.name)) - except VisionException: - logger.exception(u"{} sync failed".format(POSynchronizer.__name__)) - # Keep going to the next country - logger.info(u'Purchase orders synced successfully for {}.'.format(u', '.join(processed))) - - -@app.task -def update_tpm_partners(country_name=None): - logger.info(u'Starting update values for TPM partners') - countries = Country.objects.filter(vision_sync_enabled=True) - processed = [] - if country_name is not None: - countries = countries.filter(name=country_name) - for country in countries: - connection.set_tenant(country) - try: - logger.info(u'Starting TPM partners update for country {}'.format( - country.name - )) - for partner in TPMPartner.objects.all(): - TPMPartnerSynchronizer( - country=country, - object_number=partner.vendor_number - ).sync() - processed.append(country.name) - logger.info(u"Update finished successfully for {}".format(country.name)) - except VisionException: - logger.exception(u"{} sync failed".format(TPMPartnerSynchronizer.__name__)) - logger.info(u'TPM Partners synced successfully for {}.'.format(u', '.join(processed))) diff --git a/src/etools/applications/vision/tests/test_adapter_manual.py b/src/etools/applications/vision/tests/test_adapter_manual.py deleted file mode 100644 index d9d81358d8..0000000000 --- a/src/etools/applications/vision/tests/test_adapter_manual.py +++ /dev/null @@ -1,25 +0,0 @@ - -from django.conf import settings - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.vision.adapters import manual as adapter - - -class TestManualDataLoader(BaseTenantTestCase): - def test_init_no_endpoint_no_object_number(self): - with self.assertRaisesRegexp( - adapter.VisionException, - "You must set the ENDPOINT" - ): - adapter.ManualDataLoader() - - def test_init_no_endpoint(self): - with self.assertRaisesRegexp( - adapter.VisionException, - "You must set the ENDPOINT" - ): - adapter.ManualDataLoader(object_number="123") - - def test_init(self): - a = adapter.ManualDataLoader(endpoint="api", object_number="123") - self.assertEqual(a.url, "{}/api/123".format(settings.VISION_URL)) diff --git a/src/etools/applications/vision/tests/test_synchronizer.py b/src/etools/applications/vision/tests/test_synchronizers.py similarity index 96% rename from src/etools/applications/vision/tests/test_synchronizer.py rename to src/etools/applications/vision/tests/test_synchronizers.py index 0ddb31f065..b5944699b4 100644 --- a/src/etools/applications/vision/tests/test_synchronizer.py +++ b/src/etools/applications/vision/tests/test_synchronizers.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.test import override_settings from django.utils.timezone import now as django_now @@ -7,6 +8,7 @@ from etools.applications.users.models import Country from etools.applications.vision.exceptions import VisionException from etools.applications.vision.models import VisionSyncLog +from etools.applications.vision.synchronizers import ManualDataLoader from etools.applications.vision.vision_data_synchronizer import (VISION_NO_DATA_MESSAGE, VisionDataLoader, VisionDataSynchronizer,) @@ -391,3 +393,23 @@ def loader_get_side_effect(): self.assertEqual(mock_logger_info.call_args_list[2][1], {'exc_info': True}) self._assertVisionSyncLogFundamentals(0, 0, exception_message='Wrong!', successful=False) + + +class TestManualDataLoader(BaseTenantTestCase): + def test_init_no_endpoint_no_object_number(self): + with self.assertRaisesRegexp( + VisionException, + "You must set the ENDPOINT" + ): + ManualDataLoader() + + def test_init_no_endpoint(self): + with self.assertRaisesRegexp( + VisionException, + "You must set the ENDPOINT" + ): + ManualDataLoader(object_number="123") + + def test_init(self): + a = ManualDataLoader(endpoint="api", object_number="123") + self.assertEqual(a.url, "{}/api/123".format(settings.VISION_URL)) diff --git a/src/etools/applications/vision/tests/test_tasks.py b/src/etools/applications/vision/tests/test_tasks.py index 0edf4597f6..453b7d2282 100644 --- a/src/etools/applications/vision/tests/test_tasks.py +++ b/src/etools/applications/vision/tests/test_tasks.py @@ -10,7 +10,6 @@ import etools.applications.vision.tasks from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.users.models import Country from etools.applications.users.tests.factories import CountryFactory from etools.applications.vision.exceptions import VisionException @@ -296,23 +295,3 @@ def test_sync_country_does_not_exist(self, mock_logger): ) self.assertEqual(mock_logger.call_args[0], (expected_msg,)) self.assertEqual(mock_logger.call_args[1], {}) - - -class TestUpdatePurchaseOrders(BaseTenantTestCase): - @classmethod - def setUpTestData(cls): - cls.country = Country.objects.first() - - @mock.patch("etools.applications.vision.tasks.logger.exception") - def test_update_purchase_orders_no_country(self, mock_logger_exception): - """Ensure no exceptions if no countries""" - etools.applications.vision.tasks.update_purchase_orders(country_name="Wrong") - self.assertEqual(mock_logger_exception.call_count, 0) - - @mock.patch("etools.applications.vision.tasks.logger.exception") - @mock.patch("etools.applications.vision.tasks.POSynchronizer") - def test_update_purchase_orders(self, synchronizer, mock_logger_exception): - """Ensure no exceptions if no countries""" - etools.applications.vision.tasks.update_purchase_orders() - self.assertEqual(mock_logger_exception.call_count, 0) - self.assertEqual(synchronizer.call_count, 1) From 15f164d263d1c2f30c62a1cf2d0cd4425647c331 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 14 Sep 2018 11:19:40 -0400 Subject: [PATCH 29/42] moved sync --- .../applications/management/views/general.py | 2 +- .../applications/publics/synchronizers.py | 46 ----------------- .../applications/users/synchronizers.py | 50 +++++++++++++++++++ 3 files changed, 51 insertions(+), 47 deletions(-) create mode 100644 src/etools/applications/users/synchronizers.py diff --git a/src/etools/applications/management/views/general.py b/src/etools/applications/management/views/general.py index 8b752a3bc0..eb406c3868 100644 --- a/src/etools/applications/management/views/general.py +++ b/src/etools/applications/management/views/general.py @@ -5,8 +5,8 @@ from unicef_restlib.permissions import IsSuperUser from etools.applications.funds.synchronizers import FundReservationsSynchronizer -from etools.applications.publics.synchronizers import CountryLongNameSync from etools.applications.users.models import Country +from etools.applications.users.synchronizers import CountryLongNameSync class InvalidateCache(APIView): diff --git a/src/etools/applications/publics/synchronizers.py b/src/etools/applications/publics/synchronizers.py index 4a24f51fdf..090486a01f 100644 --- a/src/etools/applications/publics/synchronizers.py +++ b/src/etools/applications/publics/synchronizers.py @@ -216,49 +216,3 @@ def _save_records(self, records): log.info('Travel agent %s saved.', travel_agent.name) return processed - - -class CountryLongNameSync(VisionDataSynchronizer): - ENDPOINT = 'GetBusinessAreaList_JSON' - GLOBAL_CALL = True - - def __init__(self, *args, **kwargs): - self.countries_qs = Country.objects.exclude(business_area_code='0') - super(CountryLongNameSync, self).__init__(*args, **kwargs) - - def _convert_records(self, records): - records = json.loads(records['GetBusinessAreaList_JSONResult']) - records = dict((r['BUSINESS_AREA_CODE'], r) for r in records) - return records - - def _save_records(self, records): - countries = self.countries_qs.all() - countries_updated = [] - for c in countries: - try: - new_name = records[c.business_area_code]['BUSINESS_AREA_LONG_NAME'] - except KeyError: - continue - if c.long_name != new_name: - countries_updated.append((c.business_area_code, c.name, c.long_name, new_name)) - c.long_name = new_name - c.save() - - return { - 'details': '\n'.join(['Business Area: {} - {}, Old Long Name: {}, New Name: {}'.format(*c) - for c in countries_updated]), - 'total_records': len(records), # len(records), - 'processed': len(list(countries_updated)) - } - - def _filter_records(self, records): - local_business_area_codes = self.countries_qs.values_list('business_area_codes', flat=True) - - records = super()._filter_records(records) - - def bad_record(record): - if not record['BUSINESS_AREA_CODE'] in local_business_area_codes: - return False - return True - - return [rec for rec in records if bad_record(rec)] diff --git a/src/etools/applications/users/synchronizers.py b/src/etools/applications/users/synchronizers.py new file mode 100644 index 0000000000..56def07a7a --- /dev/null +++ b/src/etools/applications/users/synchronizers.py @@ -0,0 +1,50 @@ +import json + +from etools.applications.users.models import Country +from etools.applications.vision.vision_data_synchronizer import VisionDataSynchronizer + + +class CountryLongNameSync(VisionDataSynchronizer): + ENDPOINT = 'GetBusinessAreaList_JSON' + GLOBAL_CALL = True + + def __init__(self, *args, **kwargs): + self.countries_qs = Country.objects.exclude(business_area_code='0') + super(CountryLongNameSync, self).__init__(*args, **kwargs) + + def _convert_records(self, records): + records = json.loads(records['GetBusinessAreaList_JSONResult']) + records = dict((r['BUSINESS_AREA_CODE'], r) for r in records) + return records + + def _save_records(self, records): + countries = self.countries_qs.all() + countries_updated = [] + for c in countries: + try: + new_name = records[c.business_area_code]['BUSINESS_AREA_LONG_NAME'] + except KeyError: + continue + if c.long_name != new_name: + countries_updated.append((c.business_area_code, c.name, c.long_name, new_name)) + c.long_name = new_name + c.save() + + return { + 'details': '\n'.join(['Business Area: {} - {}, Old Long Name: {}, New Name: {}'.format(*c) + for c in countries_updated]), + 'total_records': len(records), # len(records), + 'processed': len(list(countries_updated)) + } + + def _filter_records(self, records): + local_business_area_codes = self.countries_qs.values_list('business_area_codes', flat=True) + + records = super()._filter_records(records) + + def bad_record(record): + if not record['BUSINESS_AREA_CODE'] in local_business_area_codes: + return False + return True + + return [rec for rec in records if bad_record(rec)] From 3e8de1c0af7541e507a06ad6e78febc34be123cd Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 17 Sep 2018 15:17:10 -0400 Subject: [PATCH 30/42] remove useless methods --- .../action_points/export/serializers.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/etools/applications/action_points/export/serializers.py b/src/etools/applications/action_points/export/serializers.py index 65118033ad..64900827b4 100644 --- a/src/etools/applications/action_points/export/serializers.py +++ b/src/etools/applications/action_points/export/serializers.py @@ -21,24 +21,3 @@ class ActionPointExportSerializer(serializers.Serializer): related_ref = serializers.CharField(source='related_object.reference_number', read_only=True, allow_null=True) related_object_str = serializers.CharField() related_object_url = serializers.CharField() - - def get_cp_output(self, obj): - return obj.cp_output.__str__ if obj.cp_output else "" - - def get_partner(self, obj): - return obj.partner.name if obj.partner else "" - - def get_office(self, obj): - return obj.office.name if obj.office else "" - - def get_section(self, obj): - return obj.section.name if obj.section else "" - - def get_category(self, obj): - return obj.category.description if obj.category else "" - - def get_pd_ssfa(self, obj): - return obj.intervention.title if obj.intervention else "" - - def get_location(self, obj): - return obj.location.__str__ if obj.location else "" From 146fbaab01c880dcdf3bdf8e51b5f7818b2c9e23 Mon Sep 17 00:00:00 2001 From: robertavram Date: Mon, 17 Sep 2018 15:57:22 -0400 Subject: [PATCH 31/42] Change handling of reporting requirements --- src/etools/applications/EquiTrack/utils.py | 5 + .../partners/serializers/interventions_v2.py | 104 +++++++++++++----- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/src/etools/applications/EquiTrack/utils.py b/src/etools/applications/EquiTrack/utils.py index 50ce0b2056..68c73bf067 100644 --- a/src/etools/applications/EquiTrack/utils.py +++ b/src/etools/applications/EquiTrack/utils.py @@ -3,6 +3,7 @@ """ import codecs import csv +import hashlib import json from datetime import datetime @@ -173,3 +174,7 @@ def get_quarter(retrieve_date=None): else: quarter = 'q4' return quarter + + +def h11(w): + return hashlib.md5(w).hexdigest()[:9] \ No newline at end of file diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 61f8b7e037..35c5970632 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -4,6 +4,7 @@ from django.db import transaction from django.db.models import Q from django.utils.translation import ugettext as _ +from django.utils.functional import cached_property from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -12,6 +13,7 @@ from unicef_locations.serializers import LocationLightSerializer, LocationSerializer from unicef_snapshot.serializers import SnapshotModelSerializer +from etools.applications.EquiTrack.utils import h11 from etools.applications.funds.models import FundsCommitmentItem, FundsReservationHeader from etools.applications.funds.serializers import FRsSerializer, FRHeaderSerializer from etools.applications.partners.models import ( @@ -764,7 +766,7 @@ def _validate_date_intervals(self, requirements): }) if i > 0: - if abs((requirements[i]["start_date"] - requirements[i - 1]["end_date"]).days) > 1: + if (requirements[i]["start_date"] - requirements[i - 1]["end_date"]).days > 1: raise serializers.ValidationError({ "reporting_requirements": { "start_date": _( @@ -773,7 +775,19 @@ def _validate_date_intervals(self, requirements): } }) - def _merge_data(self, data): + def _order_data(self, data): + data["reporting_requirements"] = sorted( + data["reporting_requirements"], + key=itemgetter("start_date") + ) + return data + + def _get_hash_key_string(self, record): + k = str(record["start_date"]) + str(record["end_date"]) + str(record["due_date"]) + return k.encode('utf-8') + + def _tweak_data(self, validated_data): + current_reqs = ReportingRequirement.objects.values( "id", "start_date", @@ -781,41 +795,39 @@ def _merge_data(self, data): "due_date", ).filter( intervention=self.intervention, - report_type=data["report_type"], + report_type=validated_data["report_type"], ) current_reqs_dict = {} - for r in current_reqs: - current_reqs_dict[r["due_date"]] = r + for c_r in current_reqs: + current_reqs_dict[h11(self._get_hash_key_string(c_r))] = c_r - report_type = data["report_type"] - for r in data["reporting_requirements"]: - # not expecting ids, so match based on due date - if r.get("due_date") in current_reqs_dict: - r["id"] = current_reqs_dict[r.get("due_date")]["id"] - current_reqs_dict.pop(r["due_date"]) + current_ids_that_need_to_be_kept = [] + + # if records don't have id but match a record with an id, assigned the id accordingly + report_type = validated_data["report_type"] + for r in validated_data["reporting_requirements"]: + if not r.get('id'): + # try to map see if the record already exists in the existing records + hash_key = h11(self._get_hash_key_string(r)) + if current_reqs_dict.get(hash_key): + r['id'] = current_reqs_dict[hash_key]['id'] + # remove record so we can track which ones are left to be deleted later on. + current_ids_that_need_to_be_kept.append(r['id']) + else: + r["intervention"] = self.intervention + else: + current_ids_that_need_to_be_kept.append(r['id']) - r["intervention"] = self.intervention r["report_type"] = report_type - if report_type == ReportingRequirement.TYPE_HR: - r["end_date"] = r["due_date"] - # We need all reporting requirements in end date order - data["reporting_requirements"] = sorted( - data["reporting_requirements"], - key=itemgetter("end_date") - ) - return data + return validated_data, current_ids_that_need_to_be_kept, current_reqs_dict def run_validation(self, initial_data): serializer = self.fields["reporting_requirements"].child - report_type = initial_data.get("report_type") - serializer.fields["start_date"].required = True - if report_type == ReportingRequirement.TYPE_QPR: - serializer.fields["end_date"].required = True - elif report_type == ReportingRequirement.TYPE_HR: - serializer.fields["due_date"].required = True + serializer.fields["end_date"].required = True + serializer.fields["due_date"].required = True return super().run_validation(initial_data) @@ -844,7 +856,19 @@ def validate(self, data): "reporting_requirements": _("This field cannot be empty.") }) - self._merge_data(data) + self._order_data(data) + # Make sure that all reporting requirements that have ids are actually related to current intervention + for rr in data["reporting_requirements"]: + try: + rr_id = rr['id'] + req = ReportingRequirement.objects.get(pk=rr_id) + assert req.intervention.id == self.intervention.id + except ReportingRequirement.DoesNotExist: + raise serializers.ValidationError("Requirement ID passed in does not exist") + except AssertionError: + raise serializers.ValidationError("Requirement ID passed in belongs to a different intervention") + except KeyError: + pass if data["report_type"] == ReportingRequirement.TYPE_QPR: self._validate_qpr(data["reporting_requirements"]) @@ -853,13 +877,39 @@ def validate(self, data): return data + @cached_property + def _past_records_editable(self): + if self.intervention.status == Intervention.DRAFT or \ + (self.intervention.status == Intervention.SIGNED and self.intervention.contingency_pd): + return True + return False + + @transaction.atomic() def create(self, validated_data): + today = date.today() + validated_data, current_ids_that_need_to_be_kept, current_reqs_dict = self._tweak_data(validated_data) + deletable_records = ReportingRequirement.objects.exclude(pk__in=current_ids_that_need_to_be_kept). \ + filter(intervention=self.intervention, report_type=validated_data['report_type']) + + if not self._past_records_editable: + if deletable_records.filter(start_date__lte=today).exists(): + raise ValidationError("You're trying to delete a record that has a start date in the past") + + deletable_records.delete() + for r in validated_data["reporting_requirements"]: if r.get("id"): pk = r.pop("id") + + # if this is a record that starts in the past and it's not editable and has changes (hash is different) + if r['start_date'] <= today and not self._past_records_editable and\ + not current_reqs_dict.get(h11(self._get_hash_key_string(r))): + raise ValidationError("A record that starts in the passed cannot" + " be modified on a non-draft PD/SSFA") ReportingRequirement.objects.filter(pk=pk).update(**r) else: ReportingRequirement.objects.create(**r) + return self.intervention def delete(self, validated_data): From cddbb48a6af07cf5d8c0e3d9e9ab72fba34e0944 Mon Sep 17 00:00:00 2001 From: robertavram Date: Mon, 17 Sep 2018 16:11:15 -0400 Subject: [PATCH 32/42] Add newline --- src/etools/applications/EquiTrack/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/EquiTrack/utils.py b/src/etools/applications/EquiTrack/utils.py index 68c73bf067..0d107d6514 100644 --- a/src/etools/applications/EquiTrack/utils.py +++ b/src/etools/applications/EquiTrack/utils.py @@ -177,4 +177,4 @@ def get_quarter(retrieve_date=None): def h11(w): - return hashlib.md5(w).hexdigest()[:9] \ No newline at end of file + return hashlib.md5(w).hexdigest()[:9] From c208d1b1a7afc38b5d0a97ce76d92a81fe4c97b6 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 17 Sep 2018 15:44:07 -0400 Subject: [PATCH 33/42] distinguish between active and hact_active --- src/etools/applications/hact/models.py | 8 +++--- src/etools/applications/hact/tasks.py | 2 +- src/etools/applications/partners/models.py | 25 +++++++++++-------- .../applications/partners/tests/test_views.py | 4 +-- .../partners/views/partner_organization_v2.py | 2 +- 5 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/etools/applications/hact/models.py b/src/etools/applications/hact/models.py index d15bf4093f..14175129f1 100644 --- a/src/etools/applications/hact/models.py +++ b/src/etools/applications/hact/models.py @@ -53,7 +53,7 @@ def update(self): @staticmethod def get_queryset(): - return PartnerOrganization.objects.active() + return PartnerOrganization.objects.hact_active() def _sum_json_values(self, qs_filters, filter_dict={}): partners = self.get_queryset().filter(**filter_dict) @@ -231,7 +231,7 @@ def get_assurance_activities(self): status=Engagement.FINAL, date_of_draft_report_to_unicef__year=datetime.now().year).count(), 'micro_assessment': MicroAssessment.objects.filter( status=Engagement.FINAL, date_of_draft_report_to_unicef__year=datetime.now().year).count(), - 'missing_micro_assessment': PartnerOrganization.objects.active( + 'missing_micro_assessment': PartnerOrganization.objects.hact_active( last_assessment_date__isnull=False, last_assessment_date__year__lte=year_limit).count(), } @@ -365,7 +365,7 @@ def get_financial_findings_numbers(): @staticmethod def get_assurance_coverage(): - qs = PartnerOrganization.objects.active() + qs = PartnerOrganization.objects.hact_active() no_coverage = qs.filter(hact_values__assurance_coverage=PartnerOrganization.ASSURANCE_VOID) partial_coverage = qs.filter(hact_values__assurance_coverage=PartnerOrganization.ASSURANCE_PARTIAL) @@ -388,7 +388,7 @@ def get_assurance_coverage(): 'table': [ { 'label': 'Active Partners', - 'value': PartnerOrganization.objects.active().count() + 'value': PartnerOrganization.objects.hact_active().count() }, { 'label': 'IPs without required PV', diff --git a/src/etools/applications/hact/tasks.py b/src/etools/applications/hact/tasks.py index 9f564368be..7d7ddb802e 100644 --- a/src/etools/applications/hact/tasks.py +++ b/src/etools/applications/hact/tasks.py @@ -25,7 +25,7 @@ def update_hact_for_country(country_name): connection.set_tenant(country) logger.info('Set country {}'.format(country_name)) try: - partners = PartnerOrganization.objects.active() + partners = PartnerOrganization.objects.hact_active() for partner in partners: logger.debug('Updating Partner {}'.format(partner.name)) partner.planned_visits_to_hact() diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 1a05e25bc6..6b2c257fa1 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -207,21 +207,26 @@ def hact_default(): class PartnerOrganizationQuerySet(models.QuerySet): def active(self, *args, **kwargs): + return self.filter(Q(total_ct_cp__gt=0) | Q(agreements__interventions__status__in=[ + Intervention.ACTIVE, Intervention.SIGNED, Intervention.SUSPENDED, Intervention.ENDED] + ), *args, **kwargs) + + def hact_active(self, *args, **kwargs): return self.filter(Q(reported_cy__gt=0) | Q(total_ct_cy__gt=0), hidden=False, *args, **kwargs) def not_programmatic_visit_compliant(self, *args, **kwargs): - return self.active(net_ct_cy__gt=PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL, - hact_values__programmatic_visits__completed__total=0, - *args, **kwargs) + return self.hact_active(net_ct_cy__gt=PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL, + hact_values__programmatic_visits__completed__total=0, + *args, **kwargs) def not_spot_check_compliant(self, *args, **kwargs): - return self.active(Q(reported_cy__gt=PartnerOrganization.CT_CP_AUDIT_TRIGGER_LEVEL) | - Q(planned_engagement__spot_check_planned_q1__gt=0) | - Q(planned_engagement__spot_check_planned_q2__gt=0) | - Q(planned_engagement__spot_check_planned_q3__gt=0) | - Q(planned_engagement__spot_check_planned_q4__gt=0), # aka required - hact_values__spot_checks__completed__total=0, - hact_values__audits__completed=0, *args, **kwargs) + return self.hact_active(Q(reported_cy__gt=PartnerOrganization.CT_CP_AUDIT_TRIGGER_LEVEL) | + Q(planned_engagement__spot_check_planned_q1__gt=0) | + Q(planned_engagement__spot_check_planned_q2__gt=0) | + Q(planned_engagement__spot_check_planned_q3__gt=0) | + Q(planned_engagement__spot_check_planned_q4__gt=0), # aka required + hact_values__spot_checks__completed__total=0, + hact_values__audits__completed=0, *args, **kwargs) def not_assurance_compliant(self, *args, **kwargs): return self.not_programmatic_visit_compliant().not_spot_check_compliant(*args, **kwargs) diff --git a/src/etools/applications/partners/tests/test_views.py b/src/etools/applications/partners/tests/test_views.py index 66d2147cb2..b6b755eb8d 100644 --- a/src/etools/applications/partners/tests/test_views.py +++ b/src/etools/applications/partners/tests/test_views.py @@ -2264,7 +2264,7 @@ def setUpTestData(cls): ta = TravelActivityFactory(travel_type=TravelType.PROGRAMME_MONITORING, date=datetime.date.today() - datetime.timedelta(200), travels=[travel, ], primary_traveler=cls.unicef_staff) - cls.intervention = InterventionFactory(agreement=agreement) + cls.intervention = InterventionFactory(agreement=agreement, status=Intervention.ACTIVE) cls.intervention.sections.set([cls.sec1, cls.sec2]) cls.intervention.flat_locations.set([cls.loc1, cls.loc2]) cls.intervention.travel_activities.set([ta, ]) @@ -2280,7 +2280,7 @@ def setUpTestData(cls): def setUp(self): self.response = self.forced_auth_req('get', reverse("partners_api:partner-dashboard"), user=self.unicef_staff) data = self.response.data - self.assertEqual(len(data), 2) + self.assertEqual(len(data), 1) self.record = data[0] def test_queryset(self): diff --git a/src/etools/applications/partners/views/partner_organization_v2.py b/src/etools/applications/partners/views/partner_organization_v2.py index 8998bd6788..06a4190fdb 100644 --- a/src/etools/applications/partners/views/partner_organization_v2.py +++ b/src/etools/applications/partners/views/partner_organization_v2.py @@ -299,7 +299,7 @@ class PartnerOrganizationHactAPIView(ListAPIView): """ permission_classes = (IsAdminUser,) queryset = PartnerOrganization.objects.select_related('planned_engagement').prefetch_related( - 'staff_members', 'assessments').active() + 'staff_members', 'assessments').hact_active() serializer_class = PartnerOrganizationHactSerializer renderer_classes = (r.JSONRenderer, PartnerOrganizationHactCsvRenderer) filename = 'detailed_hact_dashboard' From 79d142c4a0be59c52cfd1d29ff054bf7f711e602 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 17 Sep 2018 17:16:35 -0400 Subject: [PATCH 34/42] fixed core value assessment creation for new partner --- src/etools/applications/partners/synchronizers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/etools/applications/partners/synchronizers.py b/src/etools/applications/partners/synchronizers.py index 436b9f7657..b74b2f6c84 100644 --- a/src/etools/applications/partners/synchronizers.py +++ b/src/etools/applications/partners/synchronizers.py @@ -219,12 +219,11 @@ def _partner_save(self, partner, full_sync=True): if new: PlannedEngagement.objects.get_or_create(partner=partner_org) - else: - # if date has changed, archive old and create a new one not archived - core_value_date = partner_org.core_values_assessment_date - if not partner_org.core_values_assessments.filter(date=core_value_date).exists(): - partner_org.core_values_assessments.update(archived=True) - partner_org.core_values_assessments.create(date=core_value_date, archived=False) + # if date has changed, archive old and create a new one not archived + core_value_date = partner_org.core_values_assessment_date + if not partner_org.core_values_assessments.filter(date=core_value_date).exists(): + partner_org.core_values_assessments.update(archived=True) + partner_org.core_values_assessments.create(date=core_value_date, archived=False) processed = 1 From e446a354bb39f0c6e4c93c9206d72303432df5bc Mon Sep 17 00:00:00 2001 From: robertavram Date: Tue, 18 Sep 2018 11:17:16 -0400 Subject: [PATCH 35/42] Fix tests --- .../partners/serializers/interventions_v2.py | 3 +- .../partners/tests/test_api_interventions.py | 2 + .../tests/test_serializers_intervention.py | 75 +++++++++---------- .../applications/reports/tests/factories.py | 1 + 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 35c5970632..d28446822f 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -766,7 +766,8 @@ def _validate_date_intervals(self, requirements): }) if i > 0: - if (requirements[i]["start_date"] - requirements[i - 1]["end_date"]).days > 1: + if (requirements[i]["start_date"] - requirements[i - 1]["end_date"]).days > 1 or \ + (requirements[i]["start_date"] < requirements[i - 1]["end_date"]): raise serializers.ValidationError({ "reporting_requirements": { "start_date": _( diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index 179158a461..9c6cbe6a25 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -2019,9 +2019,11 @@ def test_post_hr(self): "reporting_requirements": [{ "start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15), }, { "start_date": datetime.date(2001, 4, 16), "due_date": datetime.date(2001, 5, 15), + "end_date": datetime.date(2001, 5, 15), }] } ) diff --git a/src/etools/applications/partners/tests/test_serializers_intervention.py b/src/etools/applications/partners/tests/test_serializers_intervention.py index 3aa6317bc9..38aabd60d0 100644 --- a/src/etools/applications/partners/tests/test_serializers_intervention.py +++ b/src/etools/applications/partners/tests/test_serializers_intervention.py @@ -234,30 +234,25 @@ def test_validation_hr_missing_fields(self): context=self.context ) self.assertFalse(serializer.is_valid()) - self.assertEqual( - serializer.errors['reporting_requirements'], - [{"due_date": ['This field is required.']}] - ) data["reporting_requirements"] = [{ "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15), }] serializer = InterventionReportingRequirementCreateSerializer( data=data, context=self.context ) self.assertFalse(serializer.is_valid()) - self.assertEqual( - serializer.errors['reporting_requirements'], - [{"start_date": ['This field is required.']}] - ) def test_validation_hr_indicator_invalid(self): self.assertFalse(self.indicator.is_high_frequency) data = { "report_type": ReportingRequirement.TYPE_HR, "reporting_requirements": [ - {"start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15)} + {"start_date": datetime.date(2001, 3, 15), + "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15),} ] } serializer = InterventionReportingRequirementCreateSerializer( @@ -280,6 +275,7 @@ def test_validation_hr_early_start(self): "reporting_requirements": [{ "start_date": datetime.date(2000, 1, 1), "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15), }] } serializer = InterventionReportingRequirementCreateSerializer( @@ -302,9 +298,11 @@ def test_validation_hr_due_after_next_start(self): "reporting_requirements": [{ "start_date": datetime.date(2001, 1, 1), "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15), }, { "start_date": datetime.date(2001, 2, 1), "due_date": datetime.date(2001, 5, 15), + "end_date": datetime.date(2001, 5, 15), }] } serializer = InterventionReportingRequirementCreateSerializer( @@ -327,9 +325,11 @@ def test_validation_hr_with_gaps(self): "reporting_requirements": [{ "start_date": datetime.date(2001, 1, 1), "due_date": datetime.date(2001, 3, 10), + "end_date": datetime.date(2001, 3, 10), }, { "start_date": datetime.date(2001, 4, 10), "due_date": datetime.date(2001, 5, 15), + "end_date": datetime.date(2001, 5, 15), }] } serializer = InterventionReportingRequirementCreateSerializer( @@ -350,8 +350,12 @@ def test_validation_hr(self): data = { "report_type": ReportingRequirement.TYPE_HR, "reporting_requirements": [ - {"start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15)}, - {"start_date": datetime.date(2001, 4, 16), "due_date": datetime.date(2001, 5, 15)} + {"start_date": datetime.date(2001, 3, 15), + "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15)}, + {"start_date": datetime.date(2001, 4, 16), + "due_date": datetime.date(2001, 5, 15), + "end_date": datetime.date(2001, 5, 15)} ] } serializer = InterventionReportingRequirementCreateSerializer( @@ -403,12 +407,15 @@ def test_create_hr(self): intervention=self.intervention, report_type=ReportingRequirement.TYPE_HR, ) - init_count = requirement_qs.count() data = { "report_type": ReportingRequirement.TYPE_HR, "reporting_requirements": [ - {"start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15)}, - {"start_date": datetime.date(2001, 4, 16), "due_date": datetime.date(2001, 5, 15)} + {"start_date": datetime.date(2001, 3, 15), + "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15)}, + {"start_date": datetime.date(2001, 4, 16), + "due_date": datetime.date(2001, 5, 15), + "end_date": datetime.date(2001, 5, 15)} ] } serializer = InterventionReportingRequirementCreateSerializer( @@ -417,7 +424,7 @@ def test_create_hr(self): ) self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) - self.assertEqual(requirement_qs.count(), init_count + 2) + self.assertEqual(requirement_qs.count(), 2) def test_update_qpr(self): """Updating existing qpr reporting requirements""" @@ -448,10 +455,6 @@ def test_update_qpr(self): self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) self.assertEqual(requirement_qs.count(), init_count) - req_updated = ReportingRequirement.objects.get(pk=requirement.pk) - self.assertEqual(req_updated.start_date, datetime.date(2001, 1, 1)) - self.assertEqual(req_updated.end_date, datetime.date(2001, 3, 31)) - self.assertEqual(req_updated.due_date, datetime.date(2001, 4, 15)) def test_update_hr(self): """Updating existing hr reporting requirements""" @@ -476,6 +479,7 @@ def test_update_hr(self): "reporting_requirements": [{ "start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15), }] } serializer = InterventionReportingRequirementCreateSerializer( @@ -484,11 +488,7 @@ def test_update_hr(self): ) self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) - self.assertEqual(requirement_qs.count(), init_count) - req_updated = ReportingRequirement.objects.get(pk=requirement.pk) - self.assertEqual(req_updated.end_date, req_updated.due_date) - self.assertEqual(req_updated.start_date, datetime.date(2001, 3, 15)) - self.assertEqual(req_updated.due_date, datetime.date(2001, 4, 15)) + self.assertEqual(requirement_qs.count(), 1) def test_update_create_qpr(self): """Updating existing qpr reporting requirements and create new""" @@ -497,6 +497,7 @@ def test_update_create_qpr(self): intervention=self.intervention, report_type=report_type, start_date=datetime.date(2001, 1, 3), + end_date=datetime.date(2001, 1, 15), due_date=datetime.date(2001, 4, 15), ) requirement_qs = ReportingRequirement.objects.filter( @@ -522,11 +523,7 @@ def test_update_create_qpr(self): ) self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) - self.assertEqual(requirement_qs.count(), init_count + 1) - req_updated = ReportingRequirement.objects.get(pk=requirement.pk) - self.assertEqual(req_updated.start_date, datetime.date(2001, 1, 1)) - self.assertEqual(req_updated.end_date, datetime.date(2001, 3, 31)) - self.assertEqual(req_updated.due_date, datetime.date(2001, 4, 15)) + self.assertEqual(requirement_qs.count(), 2) def test_update_create_hr(self): """Updating existing hr reporting requirements and create new""" @@ -540,19 +537,16 @@ def test_update_create_hr(self): report_type=report_type, due_date=datetime.date(2001, 4, 15), ) - requirement_qs = ReportingRequirement.objects.filter( - intervention=self.intervention, - report_type=report_type, - ) - init_count = requirement_qs.count() data = { "report_type": report_type, "reporting_requirements": [{ "start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15), + "end_date": datetime.date(2001, 4, 15), }, { "start_date": datetime.date(2001, 4, 16), "due_date": datetime.date(2001, 6, 15), + "end_date": datetime.date(2001, 6, 15), }] } serializer = InterventionReportingRequirementCreateSerializer( @@ -561,8 +555,11 @@ def test_update_create_hr(self): ) self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) - self.assertEqual(requirement_qs.count(), init_count + 1) - req_updated = ReportingRequirement.objects.get(pk=requirement.pk) - self.assertEqual(req_updated.start_date, datetime.date(2001, 3, 15)) - self.assertEqual(req_updated.end_date, req_updated.due_date) - self.assertEqual(req_updated.due_date, datetime.date(2001, 4, 15)) + + print(self.intervention) + print(report_type) + requirement_qs_count = ReportingRequirement.objects.filter( + intervention=self.intervention, + report_type=report_type, + ).count() + self.assertEqual(requirement_qs_count, 2) diff --git a/src/etools/applications/reports/tests/factories.py b/src/etools/applications/reports/tests/factories.py index a589278631..dada3af517 100644 --- a/src/etools/applications/reports/tests/factories.py +++ b/src/etools/applications/reports/tests/factories.py @@ -124,6 +124,7 @@ class Meta: report_type = fuzzy.FuzzyChoice(models.ReportingRequirement.TYPE_CHOICES) end_date = fuzzy.FuzzyDate(datetime.date(2001, 1, 1)) due_date = fuzzy.FuzzyDate(datetime.date(2001, 1, 1)) + start_date = fuzzy.FuzzyDate(datetime.date(2001, 1, 1)) class SpecialReportingRequirementFactory(factory.django.DjangoModelFactory): From 7b09ed5665d16264ca4ce90c864eca62a2a1de95 Mon Sep 17 00:00:00 2001 From: robertavram Date: Tue, 18 Sep 2018 11:28:58 -0400 Subject: [PATCH 36/42] Fix flake --- .../tests/test_serializers_intervention.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/src/etools/applications/partners/tests/test_serializers_intervention.py b/src/etools/applications/partners/tests/test_serializers_intervention.py index 38aabd60d0..158e46b3c6 100644 --- a/src/etools/applications/partners/tests/test_serializers_intervention.py +++ b/src/etools/applications/partners/tests/test_serializers_intervention.py @@ -252,7 +252,7 @@ def test_validation_hr_indicator_invalid(self): "reporting_requirements": [ {"start_date": datetime.date(2001, 3, 15), "due_date": datetime.date(2001, 4, 15), - "end_date": datetime.date(2001, 4, 15),} + "end_date": datetime.date(2001, 4, 15)} ] } serializer = InterventionReportingRequirementCreateSerializer( @@ -429,12 +429,6 @@ def test_create_hr(self): def test_update_qpr(self): """Updating existing qpr reporting requirements""" report_type = ReportingRequirement.TYPE_QPR - requirement = ReportingRequirementFactory( - intervention=self.intervention, - report_type=report_type, - start_date=datetime.date(2001, 1, 3), - due_date=datetime.date(2001, 4, 15), - ) requirement_qs = ReportingRequirement.objects.filter( intervention=self.intervention, report_type=report_type, @@ -463,17 +457,10 @@ def test_update_hr(self): lower_result=self.lower_result ) report_type = ReportingRequirement.TYPE_HR - requirement = ReportingRequirementFactory( - intervention=self.intervention, - report_type=report_type, - start_date=datetime.date(2001, 3, 15), - due_date=datetime.date(2001, 4, 15), - ) requirement_qs = ReportingRequirement.objects.filter( intervention=self.intervention, report_type=report_type, ) - init_count = requirement_qs.count() data = { "report_type": report_type, "reporting_requirements": [{ @@ -504,7 +491,6 @@ def test_update_create_qpr(self): intervention=self.intervention, report_type=report_type, ) - init_count = requirement_qs.count() data = { "report_type": report_type, "reporting_requirements": [{ @@ -532,11 +518,6 @@ def test_update_create_hr(self): lower_result=self.lower_result ) report_type = ReportingRequirement.TYPE_HR - requirement = ReportingRequirementFactory( - intervention=self.intervention, - report_type=report_type, - due_date=datetime.date(2001, 4, 15), - ) data = { "report_type": report_type, "reporting_requirements": [{ From c8e6fdb8ccca1e235b22d478f3a35fc6203a485a Mon Sep 17 00:00:00 2001 From: robertavram Date: Tue, 18 Sep 2018 11:35:37 -0400 Subject: [PATCH 37/42] Fix flake --- .../partners/tests/test_serializers_intervention.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/etools/applications/partners/tests/test_serializers_intervention.py b/src/etools/applications/partners/tests/test_serializers_intervention.py index 158e46b3c6..919e2b4021 100644 --- a/src/etools/applications/partners/tests/test_serializers_intervention.py +++ b/src/etools/applications/partners/tests/test_serializers_intervention.py @@ -480,13 +480,6 @@ def test_update_hr(self): def test_update_create_qpr(self): """Updating existing qpr reporting requirements and create new""" report_type = ReportingRequirement.TYPE_QPR - requirement = ReportingRequirementFactory( - intervention=self.intervention, - report_type=report_type, - start_date=datetime.date(2001, 1, 3), - end_date=datetime.date(2001, 1, 15), - due_date=datetime.date(2001, 4, 15), - ) requirement_qs = ReportingRequirement.objects.filter( intervention=self.intervention, report_type=report_type, From 6a116ecffb45e0b1bfd81793add9f224c7b46a04 Mon Sep 17 00:00:00 2001 From: robertavram Date: Tue, 18 Sep 2018 11:36:49 -0400 Subject: [PATCH 38/42] Remove unused import --- .../partners/tests/test_serializers_intervention.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/etools/applications/partners/tests/test_serializers_intervention.py b/src/etools/applications/partners/tests/test_serializers_intervention.py index 919e2b4021..230304773e 100644 --- a/src/etools/applications/partners/tests/test_serializers_intervention.py +++ b/src/etools/applications/partners/tests/test_serializers_intervention.py @@ -5,8 +5,7 @@ from etools.applications.partners.serializers.interventions_v2 import InterventionReportingRequirementCreateSerializer from etools.applications.partners.tests.factories import InterventionFactory, InterventionResultLinkFactory from etools.applications.reports.models import ReportingRequirement -from etools.applications.reports.tests.factories import (AppliedIndicatorFactory, LowerResultFactory, - ReportingRequirementFactory,) +from etools.applications.reports.tests.factories import (AppliedIndicatorFactory, LowerResultFactory) class TestInterventionReportingRequirementCreateSerializer(BaseTenantTestCase): From b5c4f94e1002f4b947b2bfc52362ebb9808278e8 Mon Sep 17 00:00:00 2001 From: robertavram Date: Tue, 18 Sep 2018 13:37:41 -0400 Subject: [PATCH 39/42] Fix test --- .../partners/tests/test_serializers_intervention.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/etools/applications/partners/tests/test_serializers_intervention.py b/src/etools/applications/partners/tests/test_serializers_intervention.py index 230304773e..c48888b470 100644 --- a/src/etools/applications/partners/tests/test_serializers_intervention.py +++ b/src/etools/applications/partners/tests/test_serializers_intervention.py @@ -372,7 +372,6 @@ def test_create_qpr(self): intervention=self.intervention, report_type=ReportingRequirement.TYPE_QPR, ) - init_count = requirement_qs.count() data = { "report_type": ReportingRequirement.TYPE_QPR, "reporting_requirements": [{ @@ -391,7 +390,7 @@ def test_create_qpr(self): ) self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) - self.assertEqual(requirement_qs.count(), init_count + 2) + self.assertEqual(requirement_qs.count(), 2) def test_create_hr(self): """Creating new hr reporting requirements @@ -432,7 +431,6 @@ def test_update_qpr(self): intervention=self.intervention, report_type=report_type, ) - init_count = requirement_qs.count() data = { "report_type": report_type, "reporting_requirements": [{ @@ -447,7 +445,7 @@ def test_update_qpr(self): ) self.assertTrue(serializer.is_valid()) serializer.create(serializer.validated_data) - self.assertEqual(requirement_qs.count(), init_count) + self.assertEqual(requirement_qs.count(), 1) def test_update_hr(self): """Updating existing hr reporting requirements""" From d81ccc2550660240ba26dad604cf8f9ebd59add8 Mon Sep 17 00:00:00 2001 From: robertavram Date: Tue, 18 Sep 2018 13:46:44 -0400 Subject: [PATCH 40/42] Cannot terminate a Signed PD [ch7787] --- src/etools/applications/partners/models.py | 2 +- src/etools/applications/partners/validation/interventions.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 6b2c257fa1..8daeff73b1 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -2008,7 +2008,7 @@ def basic_transition(self): pass @transition(field=status, - source=[DRAFT, SUSPENDED], + source=[DRAFT, SUSPENDED, SIGNED], target=[ACTIVE], conditions=[intervention_validation.transition_to_active], permission=intervention_validation.partnership_manager_only) diff --git a/src/etools/applications/partners/validation/interventions.py b/src/etools/applications/partners/validation/interventions.py index c86c7a0e29..6215b3948a 100644 --- a/src/etools/applications/partners/validation/interventions.py +++ b/src/etools/applications/partners/validation/interventions.py @@ -150,6 +150,10 @@ def transition_to_signed(i): def transition_to_active(i): # Only transitional validation + # this validation needs to be here in order to attempt the next auto transitional validation + if i.termination_doc_attachment.exists(): + raise TransitionError([_('Cannot Transition to ended if termination_doc attached')]) + # Validation id 1 -> if intervention is PD make sure the agreement is in active status if i.document_type in [i.PD, i.SHPD] and i.agreement.status != i.agreement.SIGNED: raise TransitionError([ From d57835c48c122a1a2909e171bbc0bdc9ba3d3915 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 19 Sep 2018 10:16:31 -0400 Subject: [PATCH 41/42] fixed serializer --- src/etools/applications/t2f/serializers/travel.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/etools/applications/t2f/serializers/travel.py b/src/etools/applications/t2f/serializers/travel.py index caa274a40c..489052d7c5 100644 --- a/src/etools/applications/t2f/serializers/travel.py +++ b/src/etools/applications/t2f/serializers/travel.py @@ -466,9 +466,8 @@ def update_related_objects(self, attr_name, related_data): class TravelListSerializer(TravelDetailsSerializer): - # TODO: reserve field names to pks for related fields and add _name for the names - traveler = serializers.CharField(source='traveler.get_full_name') - supervisor_name = serializers.CharField(source='supervisor.get_full_name') + traveler = serializers.CharField(source='traveler.get_full_name', allow_null=True) + supervisor_name = serializers.CharField(source='supervisor.get_full_name', allow_null=True) class Meta: model = Travel From 671f3b067641a5fb66e1b32d040897b98b21b362 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 19 Sep 2018 10:48:53 -0400 Subject: [PATCH 42/42] fixed active partner criteria --- src/etools/applications/partners/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 8daeff73b1..d06bd7fd62 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -207,9 +207,10 @@ def hact_default(): class PartnerOrganizationQuerySet(models.QuerySet): def active(self, *args, **kwargs): - return self.filter(Q(total_ct_cp__gt=0) | Q(agreements__interventions__status__in=[ - Intervention.ACTIVE, Intervention.SIGNED, Intervention.SUSPENDED, Intervention.ENDED] - ), *args, **kwargs) + return self.filter( + Q(partner_type=PartnerType.CIVIL_SOCIETY_ORGANIZATION, agreements__interventions__status__in=[ + Intervention.ACTIVE, Intervention.SIGNED, Intervention.SUSPENDED, Intervention.ENDED]) | + Q(total_ct_cp__gt=0), *args, **kwargs) def hact_active(self, *args, **kwargs): return self.filter(Q(reported_cy__gt=0) | Q(total_ct_cy__gt=0), hidden=False, *args, **kwargs)