From 1e7d8720c0b9298c70fedc4d822b713c25c6bf84 Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 20 Apr 2021 02:38:35 +0200 Subject: [PATCH 01/26] update postgres image for tests --- .circleci/config.yml | 2 +- docker-compose.yml | 73 ----------------------------------------- docker-compose_v2.yml | 76 ------------------------------------------- 3 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 docker-compose.yml delete mode 100644 docker-compose_v2.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fcc946fd8..f3571f38ab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,7 +50,7 @@ jobs: PGHOST: 127.0.0.1 DATABASE_URL: "postgis://postgres:postgres@localhost:5432/circle_test" DEPLOY_BRANCHES: "develop|staging|master|ci-updates2" - - image: circleci/postgres:9.5-alpine-postgis + - image: circleci/postgres:12-alpine-postgis environment: POSTGRES_USER: postgres PGUSER: postgres diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3581c261a1..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,73 +0,0 @@ -web: - image: unicef/equitrack:develop - command: python manage.py runserver - volumes: - - src:/code - ports: - - "8080:8080" - links: - - db - - redis -# - sync-gateway - environment: - - REDIS_URL=redis://redis:6379/0 - - DATABASE_URL=postgis://postgres:password@db:5432/postgres - - DJANGO_SETTINGS_MODULE=core.settings.base - - DJANGO_DEBUG=true - - COUCHBASE_URL=http://sync-gateway:4984/default - env_file: - - env_prod - -worker: - image: unicef/equitrack:develop - command: python manage.py celery worker -E --loglevel=info - links: - - db - - redis - environment: - - REDIS_URL=redis://redis:6379/0 - - DATABASE_URL=postgis://postgres:password@db:5432/postgres - -beater: - image: unicef/equitrack:develop - command: python manage.py celery beat --loglevel=info - links: - - db - - redis - environment: - - REDIS_URL=redis://redis:6379/0 - - DATABASE_URL=postgis://postgres:password@db:5432/postgres - -db: - image: mdillon/postgis:9.4 - environment: - - POSTGRES_HOST=db - - POSTGRES_PORT=5432 - - POSTGRES_NAME=postgres - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - volumes: - - ./db_dumps:/tmp/db_dumps - -redis: - image: redis - -#couchbase: -# image: couchbase/server:community-4.0.0 -# ports: -# - 8091:8091 -# - 8092:8092 -# - 8093:8093 -# - 18091:18091 -# -#sync-gateway: -# image: couchbase/sync-gateway -# command: sync-gateway.json -# volumes: -# - ./couchbase/sync-gateway.json:/sync-gateway.json -# links: -# - couchbase:couchbase -# ports: -# - 4984:4984 -# - 4985:4985 - diff --git a/docker-compose_v2.yml b/docker-compose_v2.yml deleted file mode 100644 index 5a2c1a7238..0000000000 --- a/docker-compose_v2.yml +++ /dev/null @@ -1,76 +0,0 @@ -version: '2' -services: - web: - build: - context: . - dockerfile: etools-run.docker - image: unicef/equitrack:etools - command: python manage.py runserver $PORT - ports: - - "8080:8080" - links: - - db - - redis - - sync-gateway - environment: - - REDIS_URL=redis://redis:6379/0 - - DATABASE_URL=postgis://postgres:password@db:5432/postgres - - DJANGO_SETTINGS_MODULE=core.settings.base - - DJANGO_DEBUG=true - - worker: - build: - context: . - dockerfile: etools-run.docker - command: python manage.py celery worker --loglevel=info - links: - - db - - redis - environment: - - REDIS_URL=redis://redis:6379/0 - - DATABASE_URL=postgis://postgres:password@db:5432/postgres - - beater: - build: - context: . - dockerfile: etools-run.docker - command: python manage.py celery beat --loglevel=info - links: - - db - - redis - environment: - - REDIS_URL=redis://redis:6379/0 - - DATABASE_URL=postgis://postgres:password@db:5432/postgres - - db: - image: mdillon/postgis:9.5 - environment: - - POSTGRES_HOST=db - - POSTGRES_PORT=5432 - - POSTGRES_NAME=postgres - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=password - volumes: - - ./db_dumps:/tmp/db_dumps - - redis: - image: redis - - couchbase: - image: couchbase/server:community-4.0.0 - ports: - - 8091:8091 - - 8092:8092 - - 8093:8093 - - 18091:18091 - - sync-gateway: - image: couchbase/sync-gateway - command: sync-gateway.json - volumes: - - ./couchbase/sync-gateway.json:/sync-gateway.json - links: - - couchbase:couchbase - ports: - - 4984:4984 - - 4985:4985 From 493033b6b07bf1e971a3a643a13bf6ab222e744d Mon Sep 17 00:00:00 2001 From: Domenico Date: Wed, 21 Apr 2021 23:18:09 +0200 Subject: [PATCH 02/26] 25288 site precision --- .../applications/field_monitoring/fm_settings/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/etools/applications/field_monitoring/fm_settings/serializers.py b/src/etools/applications/field_monitoring/fm_settings/serializers.py index c58ef5709d..a513954924 100644 --- a/src/etools/applications/field_monitoring/fm_settings/serializers.py +++ b/src/etools/applications/field_monitoring/fm_settings/serializers.py @@ -7,6 +7,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError +from rest_framework_gis.fields import GeometryField from unicef_attachments.fields import FileTypeModelChoiceField from unicef_attachments.models import FileType from unicef_attachments.serializers import BaseAttachmentSerializer @@ -129,6 +130,7 @@ class LocationSiteLightSerializer(serializers.ModelSerializer): (True, _('Active')), (False, _('Inactive')), ), label=_('Status'), required=False) + point = GeometryField(precision=5) class Meta: model = LocationSite From 79107ac8bf2bb7b23e50e9c2336b6f9b92ea386e Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 4 May 2021 00:13:14 +0200 Subject: [PATCH 03/26] 25736 update hact active filter --- src/etools/applications/partners/models.py | 55 ++++++++++++---------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 44e3164701..6d1dee6e82 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -605,44 +605,49 @@ def flags(self): @cached_property def min_req_programme_visits(self): programme_visits = 0 - ct = self.net_ct_cy or 0 # Must be integer, but net_ct_cy could be None - - if ct <= PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL: - programme_visits = 0 - elif PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL < ct <= PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL2: - programme_visits = 1 - elif PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL2 < ct <= PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL3: - if self.highest_risk_rating_name in [PartnerOrganization.RATING_HIGH, - PartnerOrganization.RATING_HIGH_RISK_ASSUMED, - PartnerOrganization.RATING_SIGNIFICANT]: - programme_visits = 3 - elif self.highest_risk_rating_name in [PartnerOrganization.RATING_MEDIUM, ]: - programme_visits = 2 - elif self.highest_risk_rating_name in [PartnerOrganization.RATING_LOW, - PartnerOrganization.RATING_LOW_RISK_ASSUMED]: + if self.partner_type not in [PartnerType.BILATERAL_MULTILATERAL, PartnerType.UN_AGENCY]: + ct = self.net_ct_cy or 0 # Must be integer, but net_ct_cy could be None + + if ct <= PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL: + programme_visits = 0 + elif PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL < ct <= PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL2: programme_visits = 1 - else: - if self.highest_risk_rating_name in [PartnerOrganization.RATING_HIGH, - PartnerOrganization.RATING_HIGH_RISK_ASSUMED, - PartnerOrganization.RATING_SIGNIFICANT]: - programme_visits = 4 - elif self.highest_risk_rating_name in [PartnerOrganization.RATING_MEDIUM, ]: - programme_visits = 3 - elif self.highest_risk_rating_name in [PartnerOrganization.RATING_LOW, - PartnerOrganization.RATING_LOW_RISK_ASSUMED]: - programme_visits = 2 + elif PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL2 < ct <= PartnerOrganization.CT_MR_AUDIT_TRIGGER_LEVEL3: + if self.highest_risk_rating_name in [PartnerOrganization.RATING_HIGH, + PartnerOrganization.RATING_HIGH_RISK_ASSUMED, + PartnerOrganization.RATING_SIGNIFICANT]: + programme_visits = 3 + elif self.highest_risk_rating_name in [PartnerOrganization.RATING_MEDIUM, ]: + programme_visits = 2 + elif self.highest_risk_rating_name in [PartnerOrganization.RATING_LOW, + PartnerOrganization.RATING_LOW_RISK_ASSUMED]: + programme_visits = 1 + else: + if self.highest_risk_rating_name in [PartnerOrganization.RATING_HIGH, + PartnerOrganization.RATING_HIGH_RISK_ASSUMED, + PartnerOrganization.RATING_SIGNIFICANT]: + programme_visits = 4 + elif self.highest_risk_rating_name in [PartnerOrganization.RATING_MEDIUM, ]: + programme_visits = 3 + elif self.highest_risk_rating_name in [PartnerOrganization.RATING_LOW, + PartnerOrganization.RATING_LOW_RISK_ASSUMED]: + programme_visits = 2 return programme_visits @cached_property def min_req_spot_checks(self): # reported_cy can be None reported_cy = self.reported_cy or 0 + if self.partner_type in [PartnerType.BILATERAL_MULTILATERAL, PartnerType.UN_AGENCY]: + return 0 if self.type_of_assessment == 'Low Risk Assumed' or reported_cy <= PartnerOrganization.CT_CP_AUDIT_TRIGGER_LEVEL: return 0 return 1 @cached_property def min_req_audits(self): + if self.partner_type in [PartnerType.BILATERAL_MULTILATERAL, PartnerType.UN_AGENCY]: + return 0 return self.planned_engagement.required_audit if getattr(self, 'planned_engagement', None) else 0 @cached_property From da8aef24a6756931aeb8442688c1e335f96485e2 Mon Sep 17 00:00:00 2001 From: Domenico Date: Thu, 6 May 2021 09:53:21 +0200 Subject: [PATCH 04/26] 25658 psea added fields + static api --- .../migrations/0013_auto_20210505_1620.py | 23 ++++++++++++++ src/etools/applications/psea/models.py | 31 +++++++++++++++++++ .../psea/notifications/assessment-final.py | 7 +++++ .../assessment_permissions.csv | 3 ++ src/etools/applications/psea/serializers.py | 4 +++ .../applications/psea/tests/test_models.py | 4 +++ .../applications/psea/tests/test_views.py | 18 +++++++++++ src/etools/applications/psea/urls.py | 6 +++- src/etools/applications/psea/views.py | 18 +++++++++++ 9 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/etools/applications/psea/migrations/0013_auto_20210505_1620.py diff --git a/src/etools/applications/psea/migrations/0013_auto_20210505_1620.py b/src/etools/applications/psea/migrations/0013_auto_20210505_1620.py new file mode 100644 index 0000000000..dd519e0bc3 --- /dev/null +++ b/src/etools/applications/psea/migrations/0013_auto_20210505_1620.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.20 on 2021-05-05 16:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('psea', '0012_auto_20191004_1752'), + ] + + operations = [ + migrations.AddField( + model_name='assessment', + name='assessment_ingo_reason', + field=models.CharField(blank=True, choices=[('decentralized', 'Decentralization of INGO'), ('sea_allegation', 'SEA allegation'), ('global_policy_implemented', 'Global policy not being implemented at country-level'), ('high_risk_context', 'High risk context')], max_length=16, null=True), + ), + migrations.AddField( + model_name='assessment', + name='assessment_type', + field=models.CharField(choices=[('unicef_2020', 'UNICEF Assessment 2020'), ('un_common_other', 'Assessment- Other UN'), ('un_common_unicef', 'Assessment- UNICEF')], default='unicef_2020', max_length=16), + ), + ] diff --git a/src/etools/applications/psea/models.py b/src/etools/applications/psea/models.py index 54cc8885b2..6f8bea4c86 100644 --- a/src/etools/applications/psea/models.py +++ b/src/etools/applications/psea/models.py @@ -105,6 +105,33 @@ class Assessment(TimeStampedModel): LOW_RATING = "Low" MODERATE_RATING = "Moderate" HIGH_RATING = "High" + RATING = { + LOW_RATING: LOW_RATING, + MODERATE_RATING: MODERATE_RATING, + HIGH_RATING: HIGH_RATING + } + + UNICEF_2020 = 'unicef_2020' + UN_COMMON_OTHER = 'un_common_other' + UN_COMMON_UNICEF = 'un_common_unicef' + + ASSESSMENT_TYPES = ( + (UNICEF_2020, _("UNICEF Assessment 2020")), + (UN_COMMON_OTHER, _("Assessment- Other UN")), + (UN_COMMON_UNICEF, _("Assessment- UNICEF")), + ) + + DECENTRALIZED = 'decentralized' + SEA_ALLEGATION = 'sea_allegation' + GLOBAL_POLICY_IMPLEMENTED = 'global_policy_implemented' + HIGH_RISK_CONTEXT = 'high_risk_context' + + INGO_REASONS = ( + (DECENTRALIZED, _("Decentralization of INGO")), + (SEA_ALLEGATION, _("SEA allegation")), + (GLOBAL_POLICY_IMPLEMENTED, _("Global policy not being implemented at country-level")), + (HIGH_RISK_CONTEXT, _("High risk context")), + ) reference_number = models.CharField( max_length=100, @@ -123,6 +150,8 @@ class Assessment(TimeStampedModel): null=True, blank=True, ) + assessment_type = models.CharField(max_length=16, choices=ASSESSMENT_TYPES, default=UNICEF_2020) + assessment_ingo_reason = models.CharField(max_length=16, choices=INGO_REASONS, blank=True, null=True) status = FSMField( verbose_name=_('Status'), max_length=30, @@ -241,6 +270,8 @@ def get_mail_context(self, user): "url": self.get_object_url(user=user), "overall_rating": self.overall_rating_display, "assessment_date": str(self.assessment_date), + "assessment_type": self.get_assessment_type_display(), + "assessment_ingo_reason": self.get_assessment_ingo_reason_display(), "assessor": str(self.assessor), "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in self.focal_points.all()) } diff --git a/src/etools/applications/psea/notifications/assessment-final.py b/src/etools/applications/psea/notifications/assessment-final.py index af82546c28..3043a469bc 100644 --- a/src/etools/applications/psea/notifications/assessment-final.py +++ b/src/etools/applications/psea/notifications/assessment-final.py @@ -10,6 +10,9 @@ Vendor Number: {{ partner_vendor_number }} Vendor Name: {{ partner_name }} + PSEA Assessment Type: {{ assessment_type }} + + Reason for country-level INGO assessment (if different from INGO parent): {{ assessment_ingo_reason }} SEA Risk Rating: {{ overall_rating }} @@ -34,6 +37,10 @@ Vendor Name: {{ partner_name }}

+ PSEA Assessment Type: {{ assessment_type }}

+ + Reason for country-level INGO assessment (if different from INGO parent): {{ assessment_ingo_reason }}

+ SEA Risk Rating: {{ overall_rating }}

Date of Assessment: {{ assessment_date }}

diff --git a/src/etools/applications/psea/permission_matrix/assessment_permissions.csv b/src/etools/applications/psea/permission_matrix/assessment_permissions.csv index 383b702e09..7f69a31ada 100644 --- a/src/etools/applications/psea/permission_matrix/assessment_permissions.csv +++ b/src/etools/applications/psea/permission_matrix/assessment_permissions.csv @@ -9,6 +9,9 @@ Field no,Verbose Name,Field Name,Group,Condition,Status,Action,Allowed 1.2.3,Assessment Date,assessment_date,,user belongs,in_progress,Edit,TRUE 1.2.4,Assessment Date,assessment_date,,user belongs,submitted,Edit,TRUE 1.2.4,Assessment Date,assessment_date,,user belongs,rejected,Edit,TRUE +1.2.5,Assessment Type,assessment_type,UNICEF Audit Focal Point,,draft,Required,False +1.2.5,Assessment Type,assessment_type,UNICEF Audit Focal Point,,draft,Edit,TRUE +1.2.6,Assessment INGO Reason,assessment_ingo_reason,UNICEF Audit Focal Point,,draft,Edit,TRUE 1.3.1,Assessor,assessor,UNICEF Audit Focal Point,,draft,Edit,TRUE 1.4.1,Answers,answers,,is assessor,in_progress,Edit,TRUE 1.4.2,Answers,answers,,is assessor,rejected,Edit,TRUE diff --git a/src/etools/applications/psea/serializers.py b/src/etools/applications/psea/serializers.py index 58b596d563..15d85eb5a2 100644 --- a/src/etools/applications/psea/serializers.py +++ b/src/etools/applications/psea/serializers.py @@ -162,6 +162,8 @@ class Meta(AssessmentSerializer.Meta): class AssessmentExportSerializer(AssessmentSerializer): focal_points = serializers.SerializerMethodField() overall_rating_display = serializers.ReadOnlyField(label='SEA Risk Rating') + assessment_type = serializers.ReadOnlyField(source='get_assessment_type_display') + assessment_ingo_reason = serializers.ReadOnlyField(label='get_assessment_ingo_reason_display') cs1 = serializers.SerializerMethodField() cs2 = serializers.SerializerMethodField() @@ -202,6 +204,8 @@ class Meta(AssessmentSerializer.Meta): "status", "rating", "overall_rating_display", + "assessment_type", + "assessment_ingo_reason", "assessor", "focal_points", "cs1", diff --git a/src/etools/applications/psea/tests/test_models.py b/src/etools/applications/psea/tests/test_models.py index 5d7d752e5f..ff301ae431 100644 --- a/src/etools/applications/psea/tests/test_models.py +++ b/src/etools/applications/psea/tests/test_models.py @@ -191,6 +191,8 @@ def test_get_mail_context(self): "overall_rating": assessment.overall_rating_display, "assessment_date": str(assessment.assessment_date), "assessor": str(assessment.assessor), + 'assessment_ingo_reason': None, + 'assessment_type': 'UNICEF Assessment 2020', }) def test_get_reference_number(self): @@ -236,6 +238,8 @@ def test_get_mail_context(self): "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in assessment.focal_points.all()), "assessment_date": str(assessment.assessment_date), "assessor": str(assessment.assessor), + 'assessment_ingo_reason': None, + 'assessment_type': 'UNICEF Assessment 2020', }) diff --git a/src/etools/applications/psea/tests/test_views.py b/src/etools/applications/psea/tests/test_views.py index 81d9c289e7..2b064ceb6e 100644 --- a/src/etools/applications/psea/tests/test_views.py +++ b/src/etools/applications/psea/tests/test_views.py @@ -1,4 +1,5 @@ import datetime +import json from unittest.mock import Mock, patch from django.contrib.contenttypes.models import ContentType @@ -36,6 +37,23 @@ from etools.applications.users.tests.factories import GroupFactory, UserFactory +class TestPSEAStaticDropdownsListApiView(BaseTenantTestCase): + """exercise PmpStaticDropdownsListApiView""" + + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(is_staff=True) + cls.url = reverse("psea:psea-static-list") + + def test_static(self): + + response = self.forced_auth_req('get', self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + response_json = json.loads(response.rendered_content) + self.assertIsInstance(response_json, dict) + self.assertEqual(sorted(response_json.keys()), ['ingo_reasons', 'ratings', 'types']) + + class TestAssessmentViewSet(BaseTenantTestCase): @classmethod def setUpTestData(cls): diff --git a/src/etools/applications/psea/urls.py b/src/etools/applications/psea/urls.py index aed6ed0879..8ba152fd30 100644 --- a/src/etools/applications/psea/urls.py +++ b/src/etools/applications/psea/urls.py @@ -1,9 +1,10 @@ -from django.urls import include, path +from django.urls import include, path, re_path from rest_framework_nested import routers from unicef_restlib.routers import NestedComplexRouter from etools.applications.psea import views +from etools.applications.psea.views import PSEAStaticDropdownsListAPIView root_api = routers.SimpleRouter() @@ -46,6 +47,9 @@ app_name = 'psea' urlpatterns = [ + re_path(r'^static/$', + PSEAStaticDropdownsListAPIView.as_view(http_method_names=['get']), + name='psea-static-list'), path('', include(root_api.urls)), path('', include(action_points_api.urls)), path('', include(assessor_api.urls)), diff --git a/src/etools/applications/psea/views.py b/src/etools/applications/psea/views.py index 47e900cb5e..186ed2a0f8 100644 --- a/src/etools/applications/psea/views.py +++ b/src/etools/applications/psea/views.py @@ -14,6 +14,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from unicef_attachments.models import Attachment from unicef_rest_export.renderers import ExportCSVRenderer, ExportOpenXMLRenderer from unicef_rest_export.views import ExportMixin @@ -26,6 +27,7 @@ ActionPointAuthorCondition, ) from etools.applications.audit.models import UNICEFAuditFocalPoint +from etools.applications.partners.views.v2 import choices_to_json_ready from etools.applications.permissions2.conditions import ObjectStatusCondition from etools.applications.permissions2.drf_permissions import NestedPermission from etools.applications.permissions2.metadata import PermissionBasedMetadata @@ -50,6 +52,22 @@ from etools.applications.psea.validation import AssessmentValid +class PSEAStaticDropdownsListAPIView(APIView): + permission_classes = (IsAuthenticated,) + + def get(self, request): + """Return All Static values used for dropdowns in the frontend""" + + return Response( + { + 'ratings': choices_to_json_ready(Assessment.RATING), + 'types': choices_to_json_ready(Assessment.ASSESSMENT_TYPES), + 'ingo_reasons': choices_to_json_ready(Assessment.INGO_REASONS), + }, + status=status.HTTP_200_OK + ) + + class AssessmentViewSet( SafeTenantViewSetMixin, ValidatorViewMixin, From 216e42a9308fd7feca121397ce2a75f31914a4b8 Mon Sep 17 00:00:00 2001 From: Domenico Date: Thu, 6 May 2021 10:31:42 +0200 Subject: [PATCH 05/26] 15362 partner's lead section and lead office --- src/etools/applications/partners/admin.py | 2 ++ .../migrations/0048_auto_20210506_0803.py | 25 +++++++++++++++++++ src/etools/applications/partners/models.py | 4 +++ .../partners/tests/test_export_partner.py | 12 ++++----- .../partners/tests/test_serializers.py | 2 +- 5 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 src/etools/applications/partners/migrations/0048_auto_20210506_0803.py diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index db6ae99d52..2daa514fef 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -520,6 +520,8 @@ class PartnerAdmin(ExportMixin, admin.ModelAdmin): (('name', 'vision_synced',), ('short_name', 'alternate_name',), ('partner_type', 'cso_type',), + 'lead_office', + 'lead_section', 'shared_with', 'vendor_number', 'rating', diff --git a/src/etools/applications/partners/migrations/0048_auto_20210506_0803.py b/src/etools/applications/partners/migrations/0048_auto_20210506_0803.py new file mode 100644 index 0000000000..669d98f034 --- /dev/null +++ b/src/etools/applications/partners/migrations/0048_auto_20210506_0803.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.20 on 2021-05-06 08:03 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0025_auto_20191220_2022'), + ('partners', '0047_auto_20210211_1724'), + ] + + operations = [ + migrations.AddField( + model_name='partnerorganization', + name='lead_office', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='reports.Office', verbose_name='Lead Office'), + ), + migrations.AddField( + model_name='partnerorganization', + name='lead_section', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='reports.Section', verbose_name='Lead Section'), + ), + ] diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 6d1dee6e82..e7740a88fa 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -525,6 +525,10 @@ class PartnerOrganization(TimeStampedModel): blank=True, default='', ) + lead_office = models.ForeignKey(Office, verbose_name=_("Lead Office"), + blank=True, null=True, on_delete=models.SET_NULL) + lead_section = models.ForeignKey(Section, verbose_name=_("Lead Section"), + blank=True, null=True, on_delete=models.SET_NULL) tracker = FieldTracker() objects = PartnerOrganizationQuerySet.as_manager() diff --git a/src/etools/applications/partners/tests/test_export_partner.py b/src/etools/applications/partners/tests/test_export_partner.py index e844eff277..59431d3091 100644 --- a/src/etools/applications/partners/tests/test_export_partner.py +++ b/src/etools/applications/partners/tests/test_export_partner.py @@ -153,8 +153,8 @@ def test_csv_flat_export_api(self): self.assertEqual(response.status_code, status.HTTP_200_OK) dataset = Dataset().load(response.content.decode('utf-8'), 'csv') self.assertEqual(dataset.height, 1) - self.assertEqual(len(dataset._get_headers()), 53) - self.assertEqual(len(dataset[0]), 53) + self.assertEqual(len(dataset._get_headers()), 55) + self.assertEqual(len(dataset[0]), 55) @override_settings(UNICEF_USER_EMAIL="@example.com") def test_csv_flat_export_api_hact_value_string(self): @@ -173,8 +173,8 @@ def test_csv_flat_export_api_hact_value_string(self): self.assertEqual(response.status_code, status.HTTP_200_OK) dataset = Dataset().load(response.content.decode('utf-8'), 'csv') self.assertEqual(dataset.height, 2) - self.assertEqual(len(dataset._get_headers()), 53) - self.assertEqual(len(dataset[0]), 53) + self.assertEqual(len(dataset._get_headers()), 55) + self.assertEqual(len(dataset[0]), 55) @override_settings(UNICEF_USER_EMAIL="@example.com") def test_csv_flat_export_api_hidden(self): @@ -188,8 +188,8 @@ def test_csv_flat_export_api_hidden(self): self.assertEqual(response.status_code, status.HTTP_200_OK) dataset = Dataset().load(response.content.decode('utf-8'), 'csv') self.assertEqual(dataset.height, 1) - self.assertEqual(len(dataset._get_headers()), 53) - self.assertEqual(len(dataset[0]), 53) + self.assertEqual(len(dataset._get_headers()), 55) + self.assertEqual(len(dataset[0]), 55) class TestPartnerStaffMemberModelExport(PartnerModelExportTestCase): diff --git a/src/etools/applications/partners/tests/test_serializers.py b/src/etools/applications/partners/tests/test_serializers.py index ed9bb7e99b..dbf3dcd7fb 100644 --- a/src/etools/applications/partners/tests/test_serializers.py +++ b/src/etools/applications/partners/tests/test_serializers.py @@ -706,7 +706,7 @@ def test_retrieve(self): 'vendor_number', 'vision_synced', 'planned_visits', 'manually_blocked', 'flags', 'partner_type_slug', 'outstanding_dct_amount_6_to_9_months_usd', 'outstanding_dct_amount_more_than_9_months_usd', 'psea_assessment_date', 'sea_risk_rating_name', 'highest_risk_rating_name', - 'highest_risk_rating_type', + 'highest_risk_rating_type', 'lead_office', 'lead_section' ]) self.assertCountEqual(data['planned_engagement'].keys(), [ From 3ab16a506a5caf0ad17001f4d97cd5b34fd00873 Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 24 May 2021 14:26:23 -0500 Subject: [PATCH 06/26] 25833 - psea fix assign notifications --- src/etools/applications/psea/admin.py | 21 ++++++++++++++++++- src/etools/applications/psea/models.py | 3 ++- .../assessment-action_point_assigned.py | 8 +++---- .../applications/psea/tests/test_models.py | 4 +++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/etools/applications/psea/admin.py b/src/etools/applications/psea/admin.py index 93421f79fa..b9ca673c65 100644 --- a/src/etools/applications/psea/admin.py +++ b/src/etools/applications/psea/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from etools.applications.psea.models import Answer, Assessment, Evidence, Indicator +from etools.applications.psea.models import Answer, Assessment, AssessmentActionPoint, Assessor, Evidence, Indicator @admin.register(Assessment) @@ -32,3 +32,22 @@ class EvidenceAdmin(admin.ModelAdmin): class IndicatorAdmin(admin.ModelAdmin): list_display = ('subject', 'active') list_filter = ('active',) + + +@admin.register(Assessor) +class AssessorAdmin(admin.ModelAdmin): + list_display = ('assessment', 'assessor_type', 'user', 'auditor_firm') + search_fields = ('assessment__reference_number', ) + list_filter = ('assessor_type', ) + raw_id_fields = ('user', 'assessment', 'auditor_firm', 'auditor_firm_staff') + + +@admin.register(AssessmentActionPoint) +class AssessmentActionPointAdmin(admin.ModelAdmin): + readonly_fields = ['status'] + search_fields = ('author__username', 'assigned_to__username',) + list_display = ( + 'psea_assessment', 'author', 'assigned_to', 'due_date', 'status', + ) + raw_id_fields = ('section', 'office', 'location', 'cp_output', 'partner', 'intervention', 'tpm_activity', + 'psea_assessment', 'travel_activity', 'engagement', 'author', 'assigned_by', 'assigned_to') diff --git a/src/etools/applications/psea/models.py b/src/etools/applications/psea/models.py index 6f8bea4c86..1a7d7d3328 100644 --- a/src/etools/applications/psea/models.py +++ b/src/etools/applications/psea/models.py @@ -268,12 +268,13 @@ def get_mail_context(self, user): "partner_name": self.partner.name, "partner_vendor_number": self.partner.vendor_number, "url": self.get_object_url(user=user), + 'reference_number': self.get_reference_number(), "overall_rating": self.overall_rating_display, "assessment_date": str(self.assessment_date), "assessment_type": self.get_assessment_type_display(), "assessment_ingo_reason": self.get_assessment_ingo_reason_display(), "assessor": str(self.assessor), - "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in self.focal_points.all()) + "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in self.focal_points.all()), } if self.status == self.STATUS_REJECTED: context["rejected_comment"] = self.get_rejected_comment() diff --git a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py index 767254132b..b46718be37 100644 --- a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py +++ b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py @@ -10,9 +10,9 @@ {{ action_point.assigned_by }} has assigned you an action point. - PSEA Assessment Reference Number: {{ action_point.psea_assessment.reference_number }} + PSEA Assessment Reference Number: {{ action_point.reference_number }} Due Date: {{ action_point.due_date }} - Link: {{ action_point.psea_assessment.object_url }} + Link: {{ action_point.psea_assessment.url }} Thank you. """), @@ -25,9 +25,9 @@ {{ action_point.assigned_by }} has assigned you an action point.

- PSEA Assessment Reference Number: {{ action_point.psea_assessment.reference_number }}
+ PSEA Assessment Reference Number: {{ action_point.reference_number }}
Due Date: {{ action_point.due_date }}
- Link: click here

+ Link: click here

Thank you. {% endblock %} diff --git a/src/etools/applications/psea/tests/test_models.py b/src/etools/applications/psea/tests/test_models.py index ff301ae431..61bb939a55 100644 --- a/src/etools/applications/psea/tests/test_models.py +++ b/src/etools/applications/psea/tests/test_models.py @@ -181,7 +181,7 @@ def test_user_belongs_staff(self): def test_get_mail_context(self): user = UserFactory() - assessment = AssessmentFactory() + assessment = AssessmentFactory(reference_number='TST/2021PSEA') AssessorFactory(assessment=assessment) self.assertEqual(assessment.get_mail_context(user), { "partner_name": assessment.partner.name, @@ -193,6 +193,7 @@ def test_get_mail_context(self): "assessor": str(assessment.assessor), 'assessment_ingo_reason': None, 'assessment_type': 'UNICEF Assessment 2020', + 'reference_number': 'TST/2021PSEA{}'.format(assessment.pk), }) def test_get_reference_number(self): @@ -240,6 +241,7 @@ def test_get_mail_context(self): "assessor": str(assessment.assessor), 'assessment_ingo_reason': None, 'assessment_type': 'UNICEF Assessment 2020', + 'reference_number': 'TST/2021PSEA{}'.format(assessment.pk), }) From a91566ae9ab8490d03d75942a745fffe76f47538 Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 24 May 2021 15:56:40 -0500 Subject: [PATCH 07/26] locations archived --- .../applications/field_monitoring/fm_settings/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/field_monitoring/fm_settings/models.py b/src/etools/applications/field_monitoring/fm_settings/models.py index 6fb2d87e09..99fb6589d9 100644 --- a/src/etools/applications/field_monitoring/fm_settings/models.py +++ b/src/etools/applications/field_monitoring/fm_settings/models.py @@ -204,12 +204,12 @@ def __str__(self): @staticmethod def get_parent_location(point): - locations = Location.objects.filter(geom__contains=point) + locations = Location.objects.filter(geom__contains=point, is_active=True) if locations: matched_locations = list(filter(lambda l: l.is_leaf_node(), locations)) or locations location = min(matched_locations, key=lambda l: l.geom.length) else: - location = Location.objects.filter(gateway__admin_level=0).first() + location = Location.objects.filter(gateway__admin_level=0, is_active=True).first() return location From f9e60524bae1f73d536cab4be371afe3013ff8fe Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 18 May 2021 09:13:49 -0500 Subject: [PATCH 08/26] 25658 changed psea labels --- .../psea/migrations/0014_auto_20210518_1411.py | 18 ++++++++++++++++++ src/etools/applications/psea/models.py | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/etools/applications/psea/migrations/0014_auto_20210518_1411.py diff --git a/src/etools/applications/psea/migrations/0014_auto_20210518_1411.py b/src/etools/applications/psea/migrations/0014_auto_20210518_1411.py new file mode 100644 index 0000000000..5d36ea154a --- /dev/null +++ b/src/etools/applications/psea/migrations/0014_auto_20210518_1411.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.20 on 2021-05-18 14:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('psea', '0013_auto_20210505_1620'), + ] + + operations = [ + migrations.AlterField( + model_name='assessment', + name='assessment_type', + field=models.CharField(choices=[('unicef_2020', 'UNICEF Assessment 2020'), ('un_common_other', 'UN Common Assessment- Other UN'), ('un_common_unicef', 'UN Common Assessment- UNICEF')], default='unicef_2020', max_length=16), + ), + ] diff --git a/src/etools/applications/psea/models.py b/src/etools/applications/psea/models.py index 1a7d7d3328..d01dff317c 100644 --- a/src/etools/applications/psea/models.py +++ b/src/etools/applications/psea/models.py @@ -117,8 +117,8 @@ class Assessment(TimeStampedModel): ASSESSMENT_TYPES = ( (UNICEF_2020, _("UNICEF Assessment 2020")), - (UN_COMMON_OTHER, _("Assessment- Other UN")), - (UN_COMMON_UNICEF, _("Assessment- UNICEF")), + (UN_COMMON_OTHER, _("UN Common Assessment- Other UN")), + (UN_COMMON_UNICEF, _("UN Common Assessment- UNICEF")), ) DECENTRALIZED = 'decentralized' From 9b9d68c61c49a1267ebe17a69ebe5986aba41d8d Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 1 Jun 2021 12:50:17 -0500 Subject: [PATCH 09/26] import location fix --- src/etools/libraries/locations/tasks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/etools/libraries/locations/tasks.py b/src/etools/libraries/locations/tasks.py index e0e7352dec..30a79188cc 100644 --- a/src/etools/libraries/locations/tasks.py +++ b/src/etools/libraries/locations/tasks.py @@ -7,6 +7,7 @@ from carto.exceptions import CartoException from carto.sql import SQLClient from celery.utils.log import get_task_logger +from tenant_schemas_celery.app import get_schema_name_from_task from unicef_locations.auth import LocationsCartoNoAuthClient from unicef_locations.models import CartoDBTable, Location, LocationRemapHistory from unicef_notification.utils import send_notification_with_template @@ -30,7 +31,7 @@ @celery.current_app.task(bind=True) def validate_locations_in_use(self, carto_table_pk): carto_table = CartoDBTable.objects.get(pk=carto_table_pk) - country = Country.objects.get(schema_name=self.request.headers.get('_schema_name', None)) + country = Country.objects.get(schema_name=get_schema_name_from_task(self, dict)) log, _ = get_vision_logger_domain_model().objects.get_or_create( handler_name=f'LocationsHandler ({carto_table.location_type.admin_level})', business_area_code=getattr(country, 'business_area_code', ''), @@ -89,7 +90,7 @@ def validate_locations_in_use(self, carto_table_pk): def update_sites_from_cartodb(self, carto_table_pk): carto_table = CartoDBTable.objects.get(pk=carto_table_pk) - country = Country.objects.get(schema_name=self.request.headers.get('_schema_name', None)) + country = Country.objects.get(schema_name=get_schema_name_from_task(self, dict)) log, _ = get_vision_logger_domain_model().objects.get_or_create( handler_name=f'LocationsHandler ({carto_table.location_type.admin_level})', business_area_code=getattr(country, 'business_area_code', ''), @@ -251,7 +252,7 @@ def update_sites_from_cartodb(self, carto_table_pk): @celery.current_app.task(bind=True) def cleanup_obsolete_locations(self, carto_table_pk): carto_table = CartoDBTable.objects.get(pk=carto_table_pk) - country = Country.objects.get(schema_name=self.request.headers.get('_schema_name', None)) + country = Country.objects.get(schema_name=get_schema_name_from_task(self, dict)) log, _ = get_vision_logger_domain_model().objects.get_or_create( handler_name=f'LocationsHandler ({carto_table.location_type.admin_level})', business_area_code=getattr(country, 'business_area_code', ''), From 76ec38dc1e6c9a3cae58b13e68b3f55a86e1e0e7 Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 7 Jun 2021 11:59:46 -0500 Subject: [PATCH 10/26] fix psea field: assessment_ingo_reason --- .../psea/migrations/0015_auto_20210607_1657.py | 18 ++++++++++++++++++ src/etools/applications/psea/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/etools/applications/psea/migrations/0015_auto_20210607_1657.py diff --git a/src/etools/applications/psea/migrations/0015_auto_20210607_1657.py b/src/etools/applications/psea/migrations/0015_auto_20210607_1657.py new file mode 100644 index 0000000000..afe2794b47 --- /dev/null +++ b/src/etools/applications/psea/migrations/0015_auto_20210607_1657.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.20 on 2021-06-07 16:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('psea', '0014_auto_20210518_1411'), + ] + + operations = [ + migrations.AlterField( + model_name='assessment', + name='assessment_ingo_reason', + field=models.CharField(blank=True, choices=[('decentralized', 'Decentralization of INGO'), ('sea_allegation', 'SEA allegation'), ('global_policy_implemented', 'Global policy not being implemented at country-level'), ('high_risk_context', 'High risk context')], max_length=32, null=True), + ), + ] diff --git a/src/etools/applications/psea/models.py b/src/etools/applications/psea/models.py index d01dff317c..10cabeea15 100644 --- a/src/etools/applications/psea/models.py +++ b/src/etools/applications/psea/models.py @@ -151,7 +151,7 @@ class Assessment(TimeStampedModel): blank=True, ) assessment_type = models.CharField(max_length=16, choices=ASSESSMENT_TYPES, default=UNICEF_2020) - assessment_ingo_reason = models.CharField(max_length=16, choices=INGO_REASONS, blank=True, null=True) + assessment_ingo_reason = models.CharField(max_length=32, choices=INGO_REASONS, blank=True, null=True) status = FSMField( verbose_name=_('Status'), max_length=30, From c8f48164c01b99cde0a2731158c7189ab7b66e06 Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 7 Jun 2021 11:55:00 -0500 Subject: [PATCH 11/26] prp api allow to filter by number and id --- src/etools/applications/partners/views/prp_v1.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/etools/applications/partners/views/prp_v1.py b/src/etools/applications/partners/views/prp_v1.py index e8da534ebc..9b26252947 100644 --- a/src/etools/applications/partners/views/prp_v1.py +++ b/src/etools/applications/partners/views/prp_v1.py @@ -84,6 +84,8 @@ class PRPInterventionListAPIView(QueryStringFilterMixin, ListAPIView): ) filters = ( + ('id', 'id'), + ('number', 'number'), ('country_programme', 'agreement__country_programme'), ('section', 'sections__pk'), ('status', 'status'), From 3dcce0f23b0c7909d04a6515988ac2387aca9602 Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 24 May 2021 17:57:06 -0500 Subject: [PATCH 12/26] 21780 psea individual export --- .../applications/core/templatetags/etools.py | 6 +++ src/etools/applications/psea/serializers.py | 53 ++++++++++++++++++- .../applications/psea/templates/psea_pdf.html | 39 ++++++++++++++ src/etools/applications/psea/views.py | 35 ++++++++++-- 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/etools/applications/psea/templates/psea_pdf.html diff --git a/src/etools/applications/core/templatetags/etools.py b/src/etools/applications/core/templatetags/etools.py index d96c3261cd..d22ee38aeb 100644 --- a/src/etools/applications/core/templatetags/etools.py +++ b/src/etools/applications/core/templatetags/etools.py @@ -49,3 +49,9 @@ def tenant_model_filter(context, app): def tenant_app_filter(app): tenant_app_labels = [tenant_app.split('.')[-1] for tenant_app in settings.TENANT_APPS] return app['app_label'] in tenant_app_labels + + +@register.simple_tag +def call_method(obj, method_name, *args): + method = getattr(obj, method_name) + return method(*args) diff --git a/src/etools/applications/psea/serializers.py b/src/etools/applications/psea/serializers.py index 15d85eb5a2..d3e8d326fd 100644 --- a/src/etools/applications/psea/serializers.py +++ b/src/etools/applications/psea/serializers.py @@ -163,7 +163,7 @@ class AssessmentExportSerializer(AssessmentSerializer): focal_points = serializers.SerializerMethodField() overall_rating_display = serializers.ReadOnlyField(label='SEA Risk Rating') assessment_type = serializers.ReadOnlyField(source='get_assessment_type_display') - assessment_ingo_reason = serializers.ReadOnlyField(label='get_assessment_ingo_reason_display') + assessment_ingo_reason = serializers.ReadOnlyField(source='get_assessment_ingo_reason_display') cs1 = serializers.SerializerMethodField() cs2 = serializers.SerializerMethodField() @@ -220,6 +220,57 @@ def get_focal_points(self, obj): return ", ".join([str(u) for u in obj.focal_points.all()]) +class AssessmentDetailExportSerializer(serializers.ModelSerializer): + id = serializers.ReadOnlyField(source='assessment.id') + reference_number = serializers.ReadOnlyField(source='assessment.reference_number') + assessment_date = serializers.ReadOnlyField(source='assessment.assessment_date') + partner_name = serializers.ReadOnlyField(source='assessment.partner.name') + vendor_number = serializers.ReadOnlyField(source='assessment.partner.vendor_number') + status = serializers.ReadOnlyField(source='assessment.status') + rating = serializers.ReadOnlyField(source='assessment.rating') + overall_rating_display = serializers.ReadOnlyField(source='assessment.overall_rating_display', label='SEA Risk Rating') + assessment_type = serializers.ReadOnlyField(source='assessment.get_assessment_type_display') + assessment_ingo_reason = serializers.ReadOnlyField(source='assessment.get_assessment_ingo_reason_display') + assessor = serializers.ReadOnlyField(source='assessment.assessor') + focal_points = serializers.SerializerMethodField() + cs = serializers.ReadOnlyField(source='indicator.pk') + cs_rating = serializers.ReadOnlyField(source='indicator.rating.label') + evidences = serializers.SerializerMethodField() + attachments = serializers.SerializerMethodField() + + class Meta(AssessmentSerializer.Meta): + model = Answer + fields = [ + "id", + "reference_number", + "assessment_date", + "partner_name", + "vendor_number", + "status", + "rating", + "overall_rating_display", + "assessment_type", + "assessment_ingo_reason", + "assessor", + "focal_points", + "cs", + "cs_rating", + "comments", + "evidences", + "attachments", + ] + + def get_focal_points(self, obj): + return ", ".join([str(u) for u in obj.assessment.focal_points.all()]) + + def get_evidences(self, obj): + return ", ".join([str(u) for u in obj.evidences.all()]) + + def get_attachments(self, obj): + request = self.context['request'] + return ", ".join([request.build_absolute_uri(att.file.url) for att in obj.attachments.all() if att.file]) + + class AssessmentStatusSerializer(AssessmentSerializer): class Meta(AssessmentSerializer.Meta): read_only_fields = ["reference_number", "overall_rating"] diff --git a/src/etools/applications/psea/templates/psea_pdf.html b/src/etools/applications/psea/templates/psea_pdf.html new file mode 100644 index 0000000000..95e6e6c1a0 --- /dev/null +++ b/src/etools/applications/psea/templates/psea_pdf.html @@ -0,0 +1,39 @@ +{% extends "easy_pdf/base.html" %} +{% load etools %} + +{% block content %} +
+
+
Reference Number: {{ obj.reference_number }}
+
Vendor Number: {{obj.partner.vendor_number}}
+
Name of the partner: {{obj.partner.name }}
+
Total Score: {{ obj.rating }}
+
Risk Rating: {{ obj.overall_rating_display }}
+
+ {% if qs %} + + + + + + + + + {% for item in qs %} + + + + + + + + + {% endfor %} +
Core Standard NumberCore Standard RatingCore Standard CommentsCore Standard Proof of EvidenceHyperlink to documents
{{item.indicator.pk}}{{item.rating}}{{item.comments}}{% for evidence in item.evidences.all %} {{evidence}} {% endfor %} + {% for attachment in item.attachments.all %} + {% call_method request 'build_absolute_uri' attachment.file.url %} + {% endfor %} +
+ {% endif %} +
+{% endblock %} diff --git a/src/etools/applications/psea/views.py b/src/etools/applications/psea/views.py index 186ed2a0f8..9a35844250 100644 --- a/src/etools/applications/psea/views.py +++ b/src/etools/applications/psea/views.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend +from easy_pdf.rendering import render_to_pdf_response from etools_validator.mixins import ValidatorViewMixin from rest_framework import mixins, status, viewsets from rest_framework.decorators import action @@ -41,6 +42,7 @@ AnswerSerializer, AssessmentActionPointExportSerializer, AssessmentActionPointSerializer, + AssessmentDetailExportSerializer, AssessmentDetailSerializer, AssessmentExportSerializer, AssessmentSerializer, @@ -301,14 +303,33 @@ def list_export_xlsx(self, request, *args, **kwargs): renderer_classes=(ExportOpenXMLRenderer,), ) def single_export_xlsx(self, request, *args, **kwargs): - self.serializer_class = AssessmentExportSerializer - serializer = self.get_serializer([self.get_object()], many=True) + + qs = Answer.objects.filter(assessment=self.get_object()).prefetch_related('evidences', 'attachments').order_by( + 'indicator__pk') + if qs.exists(): + self.serializer_class = AssessmentDetailExportSerializer + serializer = self.get_serializer(qs, many=True) + else: + self.serializer_class = AssessmentExportSerializer + serializer = self.get_serializer([self.get_object()], many=True) return Response(serializer.data, headers={ 'Content-Disposition': 'attachment;filename={}_{}.xlsx'.format( self.get_object().reference_number, timezone.now().date() ) }) + @action( + detail=True, + methods=['get'], + url_path='export/pdf', + ) + def single_export_pdf(self, request, *args, **kwargs): + qs = Answer.objects.filter(assessment=self.get_object()).prefetch_related('evidences', 'attachments').order_by( + 'indicator__pk') + ctx = {'obj': self.get_object(), 'qs': qs, 'request': request, 'pagesize': "A4 landscape"} + return render_to_pdf_response( + request=self.request, template='psea_pdf.html', context=ctx, filename='export.pdf') + @action( detail=False, methods=['get'], @@ -332,8 +353,14 @@ def list_export_csv(self, request, *args, **kwargs): renderer_classes=(ExportCSVRenderer,), ) def single_export_csv(self, request, *args, **kwargs): - self.serializer_class = AssessmentExportSerializer - serializer = self.get_serializer([self.get_object()], many=True) + qs = Answer.objects.filter(assessment=self.get_object()).prefetch_related('evidences', 'attachments').order_by( + 'indicator__pk') + if qs.exists(): + self.serializer_class = AssessmentDetailExportSerializer + serializer = self.get_serializer(qs, many=True) + else: + self.serializer_class = AssessmentExportSerializer + serializer = self.get_serializer([self.get_object()], many=True) return Response(serializer.data, headers={ 'Content-Disposition': 'attachment;filename={}_{}.csv'.format( self.get_object().reference_number, timezone.now().date() From 3c155ca190807a2b7d8869b318e7c108847016a0 Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 8 Jun 2021 10:53:14 -0500 Subject: [PATCH 13/26] use attachment view --- src/etools/applications/psea/serializers.py | 7 ++++++- src/etools/applications/psea/templates/psea_pdf.html | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/psea/serializers.py b/src/etools/applications/psea/serializers.py index d3e8d326fd..b67b0c49d4 100644 --- a/src/etools/applications/psea/serializers.py +++ b/src/etools/applications/psea/serializers.py @@ -1,7 +1,9 @@ from copy import copy +from urllib.parse import urljoin from django.contrib.contenttypes.models import ContentType from django.db import transaction +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -268,7 +270,10 @@ def get_evidences(self, obj): def get_attachments(self, obj): request = self.context['request'] - return ", ".join([request.build_absolute_uri(att.file.url) for att in obj.attachments.all() if att.file]) + return ", ".join(urljoin( + "https://{}".format(request.get_host()), + reverse('attachments:file', kwargs={'pk': att.pk}) + ) for att in obj.attachments.all()) class AssessmentStatusSerializer(AssessmentSerializer): diff --git a/src/etools/applications/psea/templates/psea_pdf.html b/src/etools/applications/psea/templates/psea_pdf.html index 95e6e6c1a0..61c7868a79 100644 --- a/src/etools/applications/psea/templates/psea_pdf.html +++ b/src/etools/applications/psea/templates/psea_pdf.html @@ -27,7 +27,7 @@ {% for evidence in item.evidences.all %} {{evidence}} {% endfor %} {% for attachment in item.attachments.all %} - {% call_method request 'build_absolute_uri' attachment.file.url %} + {{request.get_host}}{% url 'attachments:file' attachment.pk %} {% endfor %} From ae2e10ab2e37ab738fcca766d2daec6a4b41d5c2 Mon Sep 17 00:00:00 2001 From: ntrncic Date: Fri, 11 Jun 2021 10:12:50 -0400 Subject: [PATCH 14/26] increase value limit to 20 chars --- .../migrations/0026_auto_20210611_1403.py | 18 ++++++++++++++++++ src/etools/applications/reports/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/etools/applications/reports/migrations/0026_auto_20210611_1403.py diff --git a/src/etools/applications/reports/migrations/0026_auto_20210611_1403.py b/src/etools/applications/reports/migrations/0026_auto_20210611_1403.py new file mode 100644 index 0000000000..a71719e081 --- /dev/null +++ b/src/etools/applications/reports/migrations/0026_auto_20210611_1403.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2021-06-11 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reports', '0025_auto_20191220_2022'), + ] + + operations = [ + migrations.AlterField( + model_name='disaggregationvalue', + name='value', + field=models.CharField(max_length=20, verbose_name='Value'), + ), + ] diff --git a/src/etools/applications/reports/models.py b/src/etools/applications/reports/models.py index c9b8d150ad..42d67fb3ea 100644 --- a/src/etools/applications/reports/models.py +++ b/src/etools/applications/reports/models.py @@ -534,7 +534,7 @@ class DisaggregationValue(TimeStampedModel): verbose_name=_('Disaggregation'), on_delete=models.CASCADE, ) - value = models.CharField(max_length=15, verbose_name=_('Value')) + value = models.CharField(max_length=20, verbose_name=_('Value')) active = models.BooleanField(default=False, verbose_name=_('Active')) def __str__(self): From 0cd424476baeeadff7528e2b397b8ac2516e487d Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Mon, 14 Jun 2021 11:37:44 +0300 Subject: [PATCH 15/26] rename activity person_responsible to visit_lead --- .../data_collection/offline/synchronizer.py | 2 +- .../data_collection/tests/test_offline.py | 18 +++++----- .../data_collection/tests/test_views.py | 36 +++++++++---------- .../field_monitoring/permissions.py | 12 ++----- .../activity_validation/permissions.py | 8 ++--- .../permissions_matrix.csv | 14 ++++---- .../field_monitoring/planning/admin.py | 4 +-- .../field_monitoring/planning/filters.py | 2 +- .../migrations/0010_auto_20210614_0738.py | 18 ++++++++++ .../field_monitoring/planning/models.py | 22 ++++++------ .../notifications/activity-staff-submit.py | 6 ++-- .../field_monitoring/planning/serializers.py | 4 +-- .../field_monitoring/planning/signals.py | 4 +-- .../planning/tests/factories.py | 2 +- .../planning/tests/test_views.py | 36 +++++++++---------- .../planning/transitions/permissions.py | 4 +-- .../field_monitoring/planning/views.py | 8 ++--- .../assessment-action_point_assigned.py | 6 ++-- 18 files changed, 108 insertions(+), 98 deletions(-) create mode 100644 src/etools/applications/field_monitoring/planning/migrations/0010_auto_20210614_0738.py diff --git a/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py b/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py index 411e24f1de..4dea7d198a 100644 --- a/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py +++ b/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py @@ -31,7 +31,7 @@ def __init__(self, activity: 'MonitoringActivity'): self.enabled = not tenant_switch_is_active('fm_offline_sync_disabled') and settings.ETOOLS_OFFLINE_API def _get_data_collectors(self) -> List[str]: - data_collectors = [self.activity.person_responsible.email] + data_collectors = [self.activity.visit_lead.email] data_collectors += list(self.activity.team_members.values_list('email', flat=True)) return data_collectors diff --git a/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py b/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py index a784e454b2..45dc842480 100644 --- a/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py +++ b/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py @@ -29,12 +29,12 @@ class ChecklistBlueprintViewTestCase(APIViewSetTestCase, BaseTenantTestCase): @classmethod def setUpTestData(cls): cls.team_member = UserFactory(unicef_user=True) - cls.person_responsible = UserFactory(unicef_user=True) + cls.visit_lead = UserFactory(unicef_user=True) partner = PartnerFactory() cls.activity = MonitoringActivityFactory( status='data_collection', - person_responsible=cls.person_responsible, + visit_lead=cls.visit_lead, team_members=[cls.team_member], partners=[partner], ) @@ -161,16 +161,16 @@ def setUp(self): @patch('etools.applications.field_monitoring.data_collection.offline.synchronizer.OfflineCollect.add') def test_blueprints_sent_on_tpm_data_collection(self, add_mock): tpm_partner = TPMPartnerFactory() - person_responsible = TPMPartnerStaffMemberFactory(tpm_partner=tpm_partner).user + visit_lead = TPMPartnerStaffMemberFactory(tpm_partner=tpm_partner).user activity = MonitoringActivityFactory( status='assigned', partners=[PartnerFactory()], monitor_type='tpm', tpm_partner=tpm_partner, - person_responsible=person_responsible, team_members=[person_responsible] + visit_lead=visit_lead, team_members=[visit_lead] ) ActivityQuestionFactory(monitoring_activity=activity, is_enabled=True, question__methods=[MethodFactory()]) add_mock.reset_mock() self._test_update( - activity.person_responsible, activity, + activity.visit_lead, activity, {'status': 'data_collection'} ) add_mock.assert_called() @@ -219,12 +219,12 @@ def test_tenant_switch_disabled(self, add_mock): @override_settings(ETOOLS_OFFLINE_API='http://example.com/b/api/remote/blueprint/') @patch('etools.applications.field_monitoring.data_collection.offline.synchronizer.OfflineCollect.update') - def test_blueprint_updated_on_person_responsible_change(self, update_mock): + def test_blueprint_updated_on_visit_lead_change(self, update_mock): activity = MonitoringActivityFactory(status='data_collection', partners=[PartnerFactory()]) ActivityQuestionFactory(monitoring_activity=activity, is_enabled=True, question__methods=[MethodFactory()]) update_mock.reset_mock() - activity.person_responsible = UserFactory() + activity.visit_lead = UserFactory() activity.save() update_mock.assert_called() @@ -267,7 +267,7 @@ def test_blueprints_deleted_on_activity_report_finalization(self, delete_mock): StartedChecklistFactory(monitoring_activity=activity, method=method) delete_mock.reset_mock() - self._test_update(activity.person_responsible, activity, {'status': 'report_finalization'}) + self._test_update(activity.visit_lead, activity, {'status': 'report_finalization'}) delete_mock.assert_called() @override_settings(ETOOLS_OFFLINE_API='') @@ -296,7 +296,7 @@ def setUpTestData(cls): question__methods=[cls.method], question__answer_type='text' ) - cls.user = cls.activity.person_responsible + cls.user = cls.activity.visit_lead def get_detail_args(self, instance): return [instance.pk, self.method.pk] diff --git a/src/etools/applications/field_monitoring/data_collection/tests/test_views.py b/src/etools/applications/field_monitoring/data_collection/tests/test_views.py index cfbc14f944..b94a4d2bfa 100644 --- a/src/etools/applications/field_monitoring/data_collection/tests/test_views.py +++ b/src/etools/applications/field_monitoring/data_collection/tests/test_views.py @@ -257,12 +257,12 @@ def setUpTestData(cls): super().setUpTestData() cls.team_member = UserFactory(unicef_user=True) - cls.person_responsible = UserFactory(unicef_user=True) + cls.visit_lead = UserFactory(unicef_user=True) partner = PartnerFactory() cls.activity = MonitoringActivityFactory( status='data_collection', - person_responsible=cls.person_responsible, + visit_lead=cls.visit_lead, team_members=[cls.team_member], partners=[partner], questions__count=2, @@ -319,8 +319,8 @@ def test_information_source_depends_from_method(self): self._test_create(self.team_member, {'method': MethodFactory(use_information_source=False).pk}) - def test_start_person_responsible(self): - self._test_create(self.person_responsible, { + def test_start_visit_lead(self): + self._test_create(self.visit_lead, { 'method': MethodFactory().pk, 'information_source': 'teacher' }) @@ -339,21 +339,21 @@ def test_remove_unicef_user(self): checklist = StartedChecklistFactory(monitoring_activity=self.activity) self._test_destroy(self.unicef_user, checklist, expected_status=status.HTTP_403_FORBIDDEN) - def test_remove_person_responsible(self): + def test_remove_visit_lead(self): checklist = StartedChecklistFactory(monitoring_activity=self.activity) - self._test_destroy(self.person_responsible, checklist) + self._test_destroy(self.visit_lead, checklist) def test_remove_team_member(self): checklist = StartedChecklistFactory(monitoring_activity=self.activity) self._test_destroy(self.team_member, checklist) def test_remove_protected_in_finalize_report(self): - person_responsible = UserFactory(unicef_user=True) - activity = MonitoringActivityFactory(status='report_finalization', person_responsible=person_responsible) + visit_lead = UserFactory(unicef_user=True) + activity = MonitoringActivityFactory(status='report_finalization', visit_lead=visit_lead) original_activity, self.activity = self.activity, activity checklist = StartedChecklistFactory(monitoring_activity=activity) - self._test_destroy(person_responsible, checklist, expected_status=status.HTTP_403_FORBIDDEN) + self._test_destroy(visit_lead, checklist, expected_status=status.HTTP_403_FORBIDDEN) self.activity = original_activity @@ -379,8 +379,8 @@ def test_update_team_member(self): 'narrative_finding': 'some test text' }) - def test_update_person_responsible(self): - self._test_update(self.person_responsible, self.overall_finding, { + def test_update_visit_lead(self): + self._test_update(self.visit_lead, self.overall_finding, { 'narrative_finding': 'some test text' }) @@ -525,8 +525,8 @@ def test_update_team_member(self): 'value': 'text value' }) - def test_update_person_responsible(self): - self._test_update(self.person_responsible, self.finding, { + def test_update_visit_lead(self): + self._test_update(self.visit_lead, self.finding, { 'value': 'text value' }) @@ -569,8 +569,8 @@ def test_update_unicef(self): def test_update_team_member(self): self._test_update(self.team_member, self.overall_finding, {}, expected_status=status.HTTP_403_FORBIDDEN) - def test_update_person_responsible(self): - response = self._test_update(self.person_responsible, self.overall_finding, { + def test_update_visit_lead(self): + response = self._test_update(self.visit_lead, self.overall_finding, { 'narrative_finding': 'some test text', 'on_track': True }) @@ -623,14 +623,14 @@ def test_update_unicef(self): def test_update_team_member(self): self._test_update(self.team_member, self.overall_finding, {}, expected_status=status.HTTP_403_FORBIDDEN) - def test_update_person_responsible(self): - self._test_update(self.person_responsible, self.overall_finding, { + def test_update_visit_lead(self): + self._test_update(self.visit_lead, self.overall_finding, { 'value': 'text value' }) def test_bulk_update(self): response = self.make_list_request( - self.person_responsible, + self.visit_lead, method='patch', data=[{'id': self.overall_finding.pk, 'value': 'text value'}] ) diff --git a/src/etools/applications/field_monitoring/permissions.py b/src/etools/applications/field_monitoring/permissions.py index 0270ff533d..c1fdf545b2 100644 --- a/src/etools/applications/field_monitoring/permissions.py +++ b/src/etools/applications/field_monitoring/permissions.py @@ -81,20 +81,12 @@ def has_object_permission(self, request, view, obj): return True -class IsPersonResponsible(BasePermission): +class IsVisitLead(BasePermission): def has_permission(self, request, view): return True def has_object_permission(self, request, view, obj): - return request.user == obj.person_responsible - - -class IsActivityPersonResponsible(BasePermission): - def has_permission(self, request, view): - return request.user == view.get_root_object().person_responsible - - def has_object_permission(self, request, view, obj): - return True + return request.user == obj.visit_lead def activity_field_is_editable_permission(field): diff --git a/src/etools/applications/field_monitoring/planning/activity_validation/permissions.py b/src/etools/applications/field_monitoring/planning/activity_validation/permissions.py index 70ef0d3e6c..2c32fe0e36 100644 --- a/src/etools/applications/field_monitoring/planning/activity_validation/permissions.py +++ b/src/etools/applications/field_monitoring/planning/activity_validation/permissions.py @@ -24,17 +24,17 @@ def __init__(self, **kwargs): self.user_groups.add('All Users') - def is_person_responsible(): - return self.user == self.instance.person_responsible + def is_visit_lead(): + return self.user == self.instance.visit_lead def is_team_member(): return self.user in self.instance.team_members.all() def is_ma_user(): - return is_person_responsible() or is_team_member() + return is_visit_lead() or is_team_member() self.condition_map = { 'is_ma_related_user': is_ma_user(), - 'is_person_responsible': is_person_responsible(), + 'is_visit_lead': is_visit_lead(), 'tpm_visit+tpm_ma_related': self.instance.monitor_type == 'tpm' and is_ma_user() } diff --git a/src/etools/applications/field_monitoring/planning/activity_validation/permissions_matrix.csv b/src/etools/applications/field_monitoring/planning/activity_validation/permissions_matrix.csv index b04fad639c..af7da50cee 100644 --- a/src/etools/applications/field_monitoring/planning/activity_validation/permissions_matrix.csv +++ b/src/etools/applications/field_monitoring/planning/activity_validation/permissions_matrix.csv @@ -16,9 +16,9 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed ,monitor_type,Field Monitor,,draft,edit,TRUE ,monitor_type,Field Monitor,,checklist,edit,TRUE ,monitor_type,Field Monitor,,review,edit,TRUE -,person_responsible,Field Monitor,,draft,edit,TRUE -,person_responsible,Field Monitor,,checklist,edit,TRUE -,person_responsible,Field Monitor,,review,edit,TRUE +,visit_lead,Field Monitor,,draft,edit,TRUE +,visit_lead,Field Monitor,,checklist,edit,TRUE +,visit_lead,Field Monitor,,review,edit,TRUE ,sections,Field Monitor,,draft,edit,TRUE ,tpm_partner,Field Monitor,,draft,edit,TRUE ,tpm_partner,Field Monitor,,checklist,edit,TRUE @@ -33,7 +33,7 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed ,cp_outputs,Field Monitor,,draft,edit,TRUE ,activity_question_set,Field Monitor,,checklist,edit,TRUE ,started_checklist_set,All Users,is_ma_related_user,data_collection,edit,TRUE -,activity_overall_finding,All Users,is_person_responsible,report_finalization,edit,TRUE +,activity_overall_finding,All Users,is_visit_lead,report_finalization,edit,TRUE ,attachments,Field Monitor,,draft,edit,TRUE ,attachments,Field Monitor,,checklist,edit,TRUE ,attachments,Field Monitor,,review,edit,TRUE @@ -50,8 +50,8 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed ,location_site,All Users,,*,required,FALSE ,start_date,All Users,,assigned,required,TRUE ,end_date,All Users,,assigned,required,TRUE -,person_responsible,All Users,,assigned,required,TRUE -,person_responsible,All Users,,data_collection,required,TRUE +,visit_lead,All Users,,assigned,required,TRUE +,visit_lead,All Users,,data_collection,required,TRUE ,team_members,All Users,,assigned,required,TRUE ,team_members,All Users,,data_collection,required,TRUE @@ -63,7 +63,7 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed ,field_office,All Users,,*,view,TRUE ,offices,All Users,,*,view,TRUE ,monitor_type,All Users,,*,view,TRUE -,person_responsible,All Users,,*,view,TRUE +,visit_lead,All Users,,*,view,TRUE ,sections,All Users,,*,view,TRUE ,tpm_partner,All Users,,*,view,TRUE ,team_members,All Users,,*,view,TRUE diff --git a/src/etools/applications/field_monitoring/planning/admin.py b/src/etools/applications/field_monitoring/planning/admin.py index 6907b1d283..7b287e58df 100644 --- a/src/etools/applications/field_monitoring/planning/admin.py +++ b/src/etools/applications/field_monitoring/planning/admin.py @@ -21,8 +21,8 @@ class QuestionTemplateAdmin(admin.ModelAdmin): @admin.register(MonitoringActivity) class MonitoringActivityAdmin(admin.ModelAdmin): list_display = ( - 'reference_number', 'monitor_type', 'tpm_partner', 'person_responsible', + 'reference_number', 'monitor_type', 'tpm_partner', 'visit_lead', 'location', 'location_site', 'start_date', 'end_date', 'status' ) - list_select_related = ('tpm_partner', 'person_responsible', 'location', 'location_site') + list_select_related = ('tpm_partner', 'visit_lead', 'location', 'location_site') list_filter = ('monitor_type', 'status') diff --git a/src/etools/applications/field_monitoring/planning/filters.py b/src/etools/applications/field_monitoring/planning/filters.py index 416201631e..d590ce27bc 100644 --- a/src/etools/applications/field_monitoring/planning/filters.py +++ b/src/etools/applications/field_monitoring/planning/filters.py @@ -23,7 +23,7 @@ class Meta: 'monitor_type': ['exact'], 'tpm_partner': ['exact', 'in'], 'team_members': ['in'], - 'person_responsible': ['exact', 'in'], + 'visit_lead': ['exact', 'in'], 'location': ['exact', 'in'], 'location_site': ['exact', 'in'], 'partners': ['in'], diff --git a/src/etools/applications/field_monitoring/planning/migrations/0010_auto_20210614_0738.py b/src/etools/applications/field_monitoring/planning/migrations/0010_auto_20210614_0738.py new file mode 100644 index 0000000000..f8ad4c0e79 --- /dev/null +++ b/src/etools/applications/field_monitoring/planning/migrations/0010_auto_20210614_0738.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2021-06-14 07:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('field_monitoring_planning', '0009_auto_20210318_2046'), + ] + + operations = [ + migrations.RenameField( + model_name='monitoringactivity', + old_name='person_responsible', + new_name='visit_lead', + ), + ] diff --git a/src/etools/applications/field_monitoring/planning/models.py b/src/etools/applications/field_monitoring/planning/models.py index e79fcb7297..f2ce9702bf 100644 --- a/src/etools/applications/field_monitoring/planning/models.py +++ b/src/etools/applications/field_monitoring/planning/models.py @@ -23,7 +23,7 @@ from etools.applications.field_monitoring.planning.mixins import ProtectUnknownTransitionsMeta from etools.applications.field_monitoring.planning.transitions.permissions import ( user_is_field_monitor_permission, - user_is_person_responsible_permission, + user_is_visit_lead_permission, ) from etools.applications.partners.models import Intervention, PartnerOrganization from etools.applications.reports.models import Result, Section @@ -198,7 +198,7 @@ class MonitoringActivity( on_delete=models.CASCADE) team_members = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, verbose_name=_('Team Members'), related_name='monitoring_activities') - person_responsible = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, + visit_lead = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, verbose_name=_('Person Responsible'), related_name='+', on_delete=models.SET_NULL) @@ -234,7 +234,7 @@ class MonitoringActivity( report_reject_reason = models.TextField(verbose_name=_('Report rejection reason'), blank=True) cancel_reason = models.TextField(verbose_name=_('Cancellation reason'), blank=True) - person_responsible_tracker = FieldTracker(fields=['person_responsible']) + visit_lead_tracker = FieldTracker(fields=['visit_lead']) class Meta: verbose_name = _('Monitoring Activity') @@ -377,7 +377,7 @@ def get_mail_context(self, user=None): context = { 'object_url': object_url, - 'person_responsible': self.person_responsible.get_full_name(), + 'visit_lead': self.visit_lead.get_full_name(), 'reference_number': self.number, 'location_name': self.location.name, 'vendor_name': self.tpm_partner.name if self.tpm_partner else None, @@ -434,19 +434,19 @@ def assign(self): pass @transition(field=status, source=STATUSES.assigned, target=STATUSES.data_collection, - permission=user_is_person_responsible_permission) + permission=user_is_visit_lead_permission) def accept(self): pass def auto_accept_staff_activity(self, old_instance): # send email to users assigned to fm activity recipients = set( - list(self.team_members.all()) + [self.person_responsible] + list(self.team_members.all()) + [self.visit_lead] ) # check if it was rejected otherwise send assign message if old_instance and old_instance.status == self.STATUSES.submitted: email_template = "fm/activity/staff-reject" - recipients = [self.person_responsible] + recipients = [self.visit_lead] elif self.monitor_type == self.MONITOR_TYPE_CHOICES.staff: email_template = 'fm/activity/staff-assign' else: @@ -467,22 +467,22 @@ def auto_accept_staff_activity(self, old_instance): self.init_offline_blueprints() @transition(field=status, source=STATUSES.assigned, target=STATUSES.draft, - permission=user_is_person_responsible_permission) + permission=user_is_visit_lead_permission) def reject(self): pass @transition(field=status, source=STATUSES.data_collection, target=STATUSES.report_finalization, - permission=user_is_person_responsible_permission) + permission=user_is_visit_lead_permission) def mark_data_collected(self): pass @transition(field=status, source=STATUSES.report_finalization, target=STATUSES.data_collection, - permission=user_is_person_responsible_permission) + permission=user_is_visit_lead_permission) def revert_data_collected(self): pass @transition(field=status, source=STATUSES.report_finalization, target=STATUSES.submitted, - permission=user_is_person_responsible_permission) + permission=user_is_visit_lead_permission) def submit_report(self): pass diff --git a/src/etools/applications/field_monitoring/planning/notifications/activity-staff-submit.py b/src/etools/applications/field_monitoring/planning/notifications/activity-staff-submit.py index 66e8e921a0..70e94f2673 100644 --- a/src/etools/applications/field_monitoring/planning/notifications/activity-staff-submit.py +++ b/src/etools/applications/field_monitoring/planning/notifications/activity-staff-submit.py @@ -3,12 +3,12 @@ name = 'fm/activity/staff-submit' defaults = { 'description': 'FM Activity submitted by Staff. PME should be notified.', - 'subject': '[FM Portal] {{ activity.person_responsible }} has submitted the final report for {{ activity.reference_number }}', + 'subject': '[FM Portal] {{ activity.visit_lead }} has submitted the final report for {{ activity.reference_number }}', 'content': strip_text(""" Dear colleague, - {{ activity.person_responsible }} has submitted the final report for the Monitoring/Verification visit. + {{ activity.visit_lead }} has submitted the final report for the Monitoring/Verification visit. Please click {{ activity.object_url }} to view the final report url to activity and take the appropriate action. @@ -21,7 +21,7 @@ {% block content %} Dear colleague,

- {{ activity.person_responsible }} has submitted the final report for the Monitoring/Verification visit.
+ {{ activity.visit_lead }} has submitted the final report for the Monitoring/Verification visit.

Please click {{ activity.object_url }} to view the final report url to activity and take the appropriate action.

diff --git a/src/etools/applications/field_monitoring/planning/serializers.py b/src/etools/applications/field_monitoring/planning/serializers.py index c0e8aa2a5f..f181fae94a 100644 --- a/src/etools/applications/field_monitoring/planning/serializers.py +++ b/src/etools/applications/field_monitoring/planning/serializers.py @@ -110,7 +110,7 @@ class MonitoringActivityLightSerializer(serializers.ModelSerializer): location = SeparatedReadWriteField(read_field=LocationSerializer()) location_site = SeparatedReadWriteField(read_field=LocationSiteSerializer()) - person_responsible = SeparatedReadWriteField(read_field=MinimalUserSerializer()) + visit_lead = SeparatedReadWriteField(read_field=MinimalUserSerializer()) team_members = SeparatedReadWriteField(read_field=MinimalUserSerializer(many=True)) partners = SeparatedReadWriteField(read_field=MinimalPartnerOrganizationListSerializer(many=True)) @@ -125,7 +125,7 @@ class Meta: fields = ( 'id', 'reference_number', 'monitor_type', 'tpm_partner', - 'person_responsible', 'team_members', + 'visit_lead', 'team_members', 'location', 'location_site', 'partners', 'interventions', 'cp_outputs', 'start_date', 'end_date', diff --git a/src/etools/applications/field_monitoring/planning/signals.py b/src/etools/applications/field_monitoring/planning/signals.py index e460a9a577..7167d16b95 100644 --- a/src/etools/applications/field_monitoring/planning/signals.py +++ b/src/etools/applications/field_monitoring/planning/signals.py @@ -22,7 +22,7 @@ def update_blueprints_visibility_on_team_members_change(sender, instance, action @receiver(post_save, sender=MonitoringActivity) -def update_blueprints_visibility_on_person_responsible_changed(instance, created, **kwargs): +def update_blueprints_visibility_on_visit_lead_changed(instance, created, **kwargs): if instance.status == MonitoringActivity.STATUSES.data_collection \ - and instance.person_responsible_tracker.changed(): + and instance.visit_lead_tracker.changed(): MonitoringActivityOfflineSynchronizer(instance).update_data_collectors_list() diff --git a/src/etools/applications/field_monitoring/planning/tests/factories.py b/src/etools/applications/field_monitoring/planning/tests/factories.py index 1e8e3070c8..fefc46cfb2 100644 --- a/src/etools/applications/field_monitoring/planning/tests/factories.py +++ b/src/etools/applications/field_monitoring/planning/tests/factories.py @@ -99,7 +99,7 @@ class ReviewActivityFactory(ChecklistActivityFactory): class PreAssignedActivityFactory(ReviewActivityFactory): - person_responsible = factory.SubFactory(UserFactory, unicef_user=True) + visit_lead = factory.SubFactory(UserFactory, unicef_user=True) team_members__count = 2 diff --git a/src/etools/applications/field_monitoring/planning/tests/test_views.py b/src/etools/applications/field_monitoring/planning/tests/test_views.py index 370b4a229b..af9fe32f43 100644 --- a/src/etools/applications/field_monitoring/planning/tests/test_views.py +++ b/src/etools/applications/field_monitoring/planning/tests/test_views.py @@ -191,7 +191,7 @@ def test_dont_auto_accept_activity_if_tpm(self): tpm_partner=tpm_partner, status=MonitoringActivity.STATUSES.review, team_members=team_members, - person_responsible=UserFactory(unicef_user=True), + visit_lead=UserFactory(unicef_user=True), ) response = self._test_update(self.fm_user, activity, data={'status': 'assigned'}) @@ -222,7 +222,7 @@ def test_flow(self): partners=[PartnerFactory()], sections=[SectionFactory()] ) - person_responsible = UserFactory(unicef_user=True) + visit_lead = UserFactory(unicef_user=True) question = QuestionFactory(level=Question.LEVELS.partner, sections=activity.sections.all(), is_active=True) QuestionTemplateFactory(question=question) @@ -243,17 +243,17 @@ def goto(next_status, user, extra_data=None, mail_count=None): goto('draft', self.fm_user) goto('checklist', self.fm_user) goto('review', self.fm_user, - {'person_responsible': person_responsible.id, 'team_members': [UserFactory(unicef_user=True).id]}) + {'visit_lead': visit_lead.id, 'team_members': [UserFactory(unicef_user=True).id]}) goto('checklist', self.fm_user) goto('review', self.fm_user) goto('assigned', self.fm_user) StartedChecklistFactory(monitoring_activity=activity) - goto('report_finalization', person_responsible) + goto('report_finalization', visit_lead) ActivityOverallFinding.objects.create(monitoring_activity=activity, narrative_finding='test') - goto('data_collection', person_responsible) - goto('report_finalization', person_responsible) - goto('submitted', person_responsible, mail_count=len(PME.as_group().user_set.filter( + goto('data_collection', visit_lead) + goto('report_finalization', visit_lead) + goto('submitted', visit_lead, mail_count=len(PME.as_group().user_set.filter( profile__country=connection.tenant, ))) goto('completed', self.fm_user) @@ -265,17 +265,17 @@ def test_sections_are_displayed_correctly(self): self.assertIsNotNone(response.data['sections'][0]['name']) def test_reject_reason_required(self): - person_responsible = UserFactory(unicef_user=True) + visit_lead = UserFactory(unicef_user=True) activity = MonitoringActivityFactory(monitor_type='staff', status='assigned', - person_responsible=person_responsible) + visit_lead=visit_lead) - self._test_update(person_responsible, activity, {'status': 'draft'}, + self._test_update(visit_lead, activity, {'status': 'draft'}, expected_status=status.HTTP_400_BAD_REQUEST) - self._test_update(person_responsible, activity, {'status': 'draft', 'reject_reason': 'just because'}) + self._test_update(visit_lead, activity, {'status': 'draft', 'reject_reason': 'just because'}) def test_cancel_reason_required(self): activity = MonitoringActivityFactory(monitor_type='staff', status='assigned', - person_responsible=self.unicef_user) + visit_lead=self.unicef_user) self._test_update(self.fm_user, activity, {'status': 'cancelled'}, expected_status=status.HTTP_400_BAD_REQUEST) @@ -284,7 +284,7 @@ def test_cancel_reason_required(self): @skip("TODO: fix this test") def test_report_reject_reason_required(self): activity = MonitoringActivityFactory(monitor_type='staff', status='submitted', - person_responsible=UserFactory(unicef_user=True), + visit_lead=UserFactory(unicef_user=True), team_members=[UserFactory(unicef_user=True)]) self._test_update(self.fm_user, activity, {'status': 'report_finalization'}, @@ -294,13 +294,13 @@ def test_report_reject_reason_required(self): def test_reject_as_tpm(self): tpm_partner = SimpleTPMPartnerFactory() - person_responsible = TPMUserFactory(tpm_partner=tpm_partner) + visit_lead = TPMUserFactory(tpm_partner=tpm_partner) activity = MonitoringActivityFactory( monitor_type='tpm', status='assigned', - tpm_partner=tpm_partner, person_responsible=person_responsible, team_members=[person_responsible], + tpm_partner=tpm_partner, visit_lead=visit_lead, team_members=[visit_lead], ) - self._test_update(person_responsible, activity, {'status': 'draft', 'reject_reason': 'just because'}) + self._test_update(visit_lead, activity, {'status': 'draft', 'reject_reason': 'just because'}) self.assertEqual(len(mail.outbox), 1) def test_draft_status_permissions(self): @@ -311,7 +311,7 @@ def test_draft_status_permissions(self): self.assertTrue(permissions['edit']['sections']) self.assertTrue(permissions['edit']['team_members']) - self.assertTrue(permissions['edit']['person_responsible']) + self.assertTrue(permissions['edit']['visit_lead']) self.assertTrue(permissions['edit']['interventions']) self.assertFalse(permissions['edit']['activity_question_set']) self.assertFalse(permissions['view']['additional_info']) @@ -324,7 +324,7 @@ def test_checklist_status_permissions(self): self.assertFalse(permissions['edit']['sections']) self.assertTrue(permissions['edit']['team_members']) - self.assertTrue(permissions['edit']['person_responsible']) + self.assertTrue(permissions['edit']['visit_lead']) self.assertTrue(permissions['edit']['activity_question_set']) self.assertFalse(permissions['view']['activity_question_set_review']) self.assertTrue(permissions['view']['additional_info']) diff --git a/src/etools/applications/field_monitoring/planning/transitions/permissions.py b/src/etools/applications/field_monitoring/planning/transitions/permissions.py index de2c46c3d6..9113501fd8 100644 --- a/src/etools/applications/field_monitoring/planning/transitions/permissions.py +++ b/src/etools/applications/field_monitoring/planning/transitions/permissions.py @@ -8,5 +8,5 @@ def user_is_field_monitor_permission(activity, user): return False -def user_is_person_responsible_permission(activity, user): - return user == activity.person_responsible +def user_is_visit_lead_permission(activity, user): + return user == activity.visit_lead diff --git a/src/etools/applications/field_monitoring/planning/views.py b/src/etools/applications/field_monitoring/planning/views.py index 755f86f97d..3a14688bd6 100644 --- a/src/etools/applications/field_monitoring/planning/views.py +++ b/src/etools/applications/field_monitoring/planning/views.py @@ -27,7 +27,7 @@ IsFieldMonitor, IsListAction, IsObjectAction, - IsPersonResponsible, + IsVisitLead, IsReadAction, ) from etools.applications.field_monitoring.planning.activity_validation.validator import ActivityValid @@ -141,7 +141,7 @@ class MonitoringActivitiesViewSet( Retrieve and Update Agreement. """ queryset = MonitoringActivity.objects.annotate(checklists_count=Count('checklists')).select_related( - 'tpm_partner', 'person_responsible', 'location__gateway', 'location_site', + 'tpm_partner', 'visit_lead', 'location__gateway', 'location_site', ).prefetch_related( 'team_members', 'partners', 'interventions', 'cp_outputs' ).order_by("-id") @@ -152,7 +152,7 @@ class MonitoringActivitiesViewSet( permission_classes = FMBaseViewSet.permission_classes + [ IsReadAction | (IsEditAction & IsListAction & IsFieldMonitor) | - (IsEditAction & (IsObjectAction & (IsFieldMonitor | IsPersonResponsible))) + (IsEditAction & (IsObjectAction & (IsFieldMonitor | IsVisitLead))) ] filter_backends = (DjangoFilterBackend, ReferenceNumberOrderingFilter, OrderingFilter, SearchFilter) filter_class = MonitoringActivitiesFilterSet @@ -169,7 +169,7 @@ def get_queryset(self): # we should hide activities before assignment # if reject reason available activity should be visible (draft + reject_reason = rejected) queryset = queryset.filter( - Q(person_responsible=self.request.user) | Q(team_members=self.request.user), + Q(visit_lead=self.request.user) | Q(team_members=self.request.user), Q(status__in=MonitoringActivity.TPM_AVAILABLE_STATUSES) | ~Q(reject_reason=''), ) diff --git a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py index b46718be37..4d55dd7e0d 100644 --- a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py +++ b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py @@ -3,10 +3,10 @@ name = 'psea/assessment/action_point_assigned' defaults = { 'description': 'PSEA Assessment action point was assigned', - 'subject': '[eTools] ACTION POINT ASSIGNED to {{ action_point.person_responsible }}', + 'subject': '[eTools] ACTION POINT ASSIGNED to {{ action_point.visit_lead }}', 'content': strip_text(""" - Dear {{ action_point.person_responsible }}, + Dear {{ action_point.visit_lead }}, {{ action_point.assigned_by }} has assigned you an action point. @@ -21,7 +21,7 @@ {% extends "email-templates/base" %} {% block content %} - Dear {{ action_point.person_responsible }},

+ Dear {{ action_point.visit_lead }},

{{ action_point.assigned_by }} has assigned you an action point.

From 41d56ef6225ad181c0d815cce2a918e057452fcf Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Mon, 14 Jun 2021 11:50:49 +0300 Subject: [PATCH 16/26] only pme is allowed to accept final report --- .../applications/field_monitoring/planning/models.py | 9 +++++---- .../field_monitoring/planning/transitions/permissions.py | 4 ++++ .../applications/field_monitoring/planning/views.py | 2 +- utils/trigger_redeploy.py | 5 +++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/etools/applications/field_monitoring/planning/models.py b/src/etools/applications/field_monitoring/planning/models.py index f2ce9702bf..b653569fe3 100644 --- a/src/etools/applications/field_monitoring/planning/models.py +++ b/src/etools/applications/field_monitoring/planning/models.py @@ -23,6 +23,7 @@ from etools.applications.field_monitoring.planning.mixins import ProtectUnknownTransitionsMeta from etools.applications.field_monitoring.planning.transitions.permissions import ( user_is_field_monitor_permission, + user_is_pme_permission, user_is_visit_lead_permission, ) from etools.applications.partners.models import Intervention, PartnerOrganization @@ -199,8 +200,8 @@ class MonitoringActivity( team_members = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, verbose_name=_('Team Members'), related_name='monitoring_activities') visit_lead = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, - verbose_name=_('Person Responsible'), related_name='+', - on_delete=models.SET_NULL) + verbose_name=_('Person Responsible'), related_name='+', + on_delete=models.SET_NULL) field_office = models.ForeignKey('reports.Office', blank=True, null=True, verbose_name=_('Field Office'), on_delete=models.CASCADE) @@ -487,12 +488,12 @@ def submit_report(self): pass @transition(field=status, source=STATUSES.submitted, target=STATUSES.completed, - permission=user_is_field_monitor_permission) + permission=user_is_pme_permission) def complete(self): pass @transition(field=status, source=STATUSES.submitted, target=STATUSES.report_finalization, - permission=user_is_field_monitor_permission) + permission=user_is_pme_permission) def reject_report(self): pass diff --git a/src/etools/applications/field_monitoring/planning/transitions/permissions.py b/src/etools/applications/field_monitoring/planning/transitions/permissions.py index 9113501fd8..bebe0134d8 100644 --- a/src/etools/applications/field_monitoring/planning/transitions/permissions.py +++ b/src/etools/applications/field_monitoring/planning/transitions/permissions.py @@ -2,6 +2,10 @@ from etools.applications.tpm.models import PME +def user_is_pme_permission(activity, user): + return PME.as_group() in user.groups.all() + + def user_is_field_monitor_permission(activity, user): if {FMUser.as_group(), PME.as_group()}.intersection(user.groups.all()): return True diff --git a/src/etools/applications/field_monitoring/planning/views.py b/src/etools/applications/field_monitoring/planning/views.py index 3a14688bd6..9e0cb64aa7 100644 --- a/src/etools/applications/field_monitoring/planning/views.py +++ b/src/etools/applications/field_monitoring/planning/views.py @@ -27,8 +27,8 @@ IsFieldMonitor, IsListAction, IsObjectAction, - IsVisitLead, IsReadAction, + IsVisitLead, ) from etools.applications.field_monitoring.planning.activity_validation.validator import ActivityValid from etools.applications.field_monitoring.planning.filters import ( diff --git a/utils/trigger_redeploy.py b/utils/trigger_redeploy.py index 6eedbad407..5ad3713d0b 100644 --- a/utils/trigger_redeploy.py +++ b/utils/trigger_redeploy.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- import argparse -from datetime import datetime import logging import os -import requests import sys +from datetime import datetime + +import requests logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) From 7882006dd6caa6a8e49ff9a4e6617c954c9b1965 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Mon, 14 Jun 2021 13:14:02 +0300 Subject: [PATCH 17/26] fix test --- .../applications/field_monitoring/planning/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/field_monitoring/planning/tests/test_views.py b/src/etools/applications/field_monitoring/planning/tests/test_views.py index af9fe32f43..a42a2e009a 100644 --- a/src/etools/applications/field_monitoring/planning/tests/test_views.py +++ b/src/etools/applications/field_monitoring/planning/tests/test_views.py @@ -256,7 +256,7 @@ def goto(next_status, user, extra_data=None, mail_count=None): goto('submitted', visit_lead, mail_count=len(PME.as_group().user_set.filter( profile__country=connection.tenant, ))) - goto('completed', self.fm_user) + goto('completed', self.pme) def test_sections_are_displayed_correctly(self): activity = MonitoringActivityFactory(status=MonitoringActivity.STATUSES.draft, sections=[SectionFactory()]) From 3b4fce03b038154daca91678579203c656184506 Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Tue, 15 Jun 2021 15:02:39 +0300 Subject: [PATCH 18/26] add test to filter activities by visit lead --- .../field_monitoring/planning/tests/test_views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/etools/applications/field_monitoring/planning/tests/test_views.py b/src/etools/applications/field_monitoring/planning/tests/test_views.py index a42a2e009a..df52e40dde 100644 --- a/src/etools/applications/field_monitoring/planning/tests/test_views.py +++ b/src/etools/applications/field_monitoring/planning/tests/test_views.py @@ -116,6 +116,16 @@ def test_search_by_ref_number(self): self._test_list(self.unicef_user, [activity], data={'search': activity.reference_number}) + def test_filter_by_visit_lead(self): + activity1 = MonitoringActivityFactory(monitor_type='staff', visit_lead=UserFactory()) + activity2 = MonitoringActivityFactory(monitor_type='staff', visit_lead=UserFactory()) + MonitoringActivityFactory(monitor_type='staff', visit_lead=UserFactory()) + + self._test_list( + self.unicef_user, [activity1, activity2], + data={'visit_lead__in': f'{activity1.visit_lead.pk},{activity2.visit_lead.pk}'} + ) + def test_details(self): activity = MonitoringActivityFactory(monitor_type='staff', team_members=[UserFactory(unicef_user=True)]) From 74d6977507e7aa884781f3985d763c713a84e52b Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 15 Jun 2021 09:03:59 -0500 Subject: [PATCH 19/26] 21780-psea-export-layout --- .../applications/psea/templates/psea_pdf.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/etools/applications/psea/templates/psea_pdf.html b/src/etools/applications/psea/templates/psea_pdf.html index 61c7868a79..2ac77ae03e 100644 --- a/src/etools/applications/psea/templates/psea_pdf.html +++ b/src/etools/applications/psea/templates/psea_pdf.html @@ -13,18 +13,22 @@ {% if qs %} - - - - - + + + + + {% for item in qs %} - +
Core Standard NumberCore Standard RatingCore Standard CommentsCore Standard Proof of EvidenceHyperlink to documentsCore Standard NumberCore Standard RatingCore Standard CommentsCore Standard Proof of EvidenceHyperlink to documents
{{item.indicator.pk}} {{item.rating}} {{item.comments}}{% for evidence in item.evidences.all %} {{evidence}} {% endfor %} + {% for evidence in item.evidences.all %} + {{evidence}}
+ {% endfor %} +
{% for attachment in item.attachments.all %} {{request.get_host}}{% url 'attachments:file' attachment.pk %} From 9d7a8cbcd5f01c3fe2f8d5fce99c19940dbd8271 Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 15 Jun 2021 13:39:59 -0500 Subject: [PATCH 20/26] 21780 psea amend export --- src/etools/applications/psea/serializers.py | 56 ++++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/etools/applications/psea/serializers.py b/src/etools/applications/psea/serializers.py index b67b0c49d4..71ec4c69f6 100644 --- a/src/etools/applications/psea/serializers.py +++ b/src/etools/applications/psea/serializers.py @@ -223,45 +223,51 @@ def get_focal_points(self, obj): class AssessmentDetailExportSerializer(serializers.ModelSerializer): - id = serializers.ReadOnlyField(source='assessment.id') - reference_number = serializers.ReadOnlyField(source='assessment.reference_number') - assessment_date = serializers.ReadOnlyField(source='assessment.assessment_date') + # id = serializers.ReadOnlyField(source='assessment.id') + # reference_number = serializers.ReadOnlyField(source='assessment.reference_number') + # assessment_date = serializers.ReadOnlyField(source='assessment.assessment_date') partner_name = serializers.ReadOnlyField(source='assessment.partner.name') vendor_number = serializers.ReadOnlyField(source='assessment.partner.vendor_number') - status = serializers.ReadOnlyField(source='assessment.status') - rating = serializers.ReadOnlyField(source='assessment.rating') - overall_rating_display = serializers.ReadOnlyField(source='assessment.overall_rating_display', label='SEA Risk Rating') - assessment_type = serializers.ReadOnlyField(source='assessment.get_assessment_type_display') - assessment_ingo_reason = serializers.ReadOnlyField(source='assessment.get_assessment_ingo_reason_display') - assessor = serializers.ReadOnlyField(source='assessment.assessor') - focal_points = serializers.SerializerMethodField() - cs = serializers.ReadOnlyField(source='indicator.pk') - cs_rating = serializers.ReadOnlyField(source='indicator.rating.label') - evidences = serializers.SerializerMethodField() - attachments = serializers.SerializerMethodField() + # status = serializers.ReadOnlyField(source='assessment.status') + total_score = serializers.ReadOnlyField(source='assessment.rating') + overall_rating_display = serializers.ReadOnlyField(source='assessment.overall_rating_display', label='Risk Rating') + # assessment_type = serializers.ReadOnlyField(source='assessment.get_assessment_type_display') + # assessment_ingo_reason = serializers.ReadOnlyField(source='assessment.get_assessment_ingo_reason_display') + # assessor = serializers.ReadOnlyField(source='assessment.assessor') + # focal_points = serializers.SerializerMethodField() + # rating = serializers.ReadOnlyField(source='indicator.rating') + cs = serializers.SerializerMethodField(label='Core standard number') + core_standard_rating = serializers.ReadOnlyField(source='rating') + comments = serializers.ReadOnlyField(label='Core standard comments') + evidences = serializers.SerializerMethodField(label='Core standard proof of evidence') + attachments = serializers.SerializerMethodField(label='Hyperlink to documents') class Meta(AssessmentSerializer.Meta): model = Answer fields = [ - "id", - "reference_number", - "assessment_date", - "partner_name", + # "id", + # "reference_number", + # "status", + # "assessment_date", "vendor_number", - "status", - "rating", + "partner_name", + # "overall_rating", + "total_score", "overall_rating_display", - "assessment_type", - "assessment_ingo_reason", - "assessor", - "focal_points", + # "assessment_type", + # "assessment_ingo_reason", + # "assessor", + # "focal_points", "cs", - "cs_rating", + "core_standard_rating", "comments", "evidences", "attachments", ] + def get_cs(self, obj): + return f'CS{obj.indicator.pk}' + def get_focal_points(self, obj): return ", ".join([str(u) for u in obj.assessment.focal_points.all()]) From cd81b31802aa87116467408a50836b22913efd97 Mon Sep 17 00:00:00 2001 From: Domenico Date: Thu, 17 Jun 2021 15:43:21 -0500 Subject: [PATCH 21/26] psea 25659 psea nfr attachment --- .../applications/attachments/serializers.py | 22 ++++++++++++++++ src/etools/applications/attachments/views.py | 26 +++++++++++++++++++ .../core/data/attachments_file_types.json | 11 ++++++++ src/etools/applications/psea/admin.py | 11 ++++++++ src/etools/applications/psea/models.py | 11 ++++++++ .../psea/notifications/assessment-final.py | 8 ++++++ .../assessment_permissions.csv | 2 ++ src/etools/applications/psea/serializers.py | 12 +++++++-- .../applications/psea/tests/test_models.py | 2 ++ src/etools/applications/psea/urls.py | 1 + src/etools/applications/psea/views.py | 14 ++++++++-- src/etools/applications/tpm/admin.py | 16 +++++++++--- 12 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 src/etools/applications/attachments/serializers.py create mode 100644 src/etools/applications/attachments/views.py diff --git a/src/etools/applications/attachments/serializers.py b/src/etools/applications/attachments/serializers.py new file mode 100644 index 0000000000..a61de9bcc0 --- /dev/null +++ b/src/etools/applications/attachments/serializers.py @@ -0,0 +1,22 @@ +from django.utils.translation import gettext_lazy as _ + +from unicef_attachments.fields import FileTypeModelChoiceField +from unicef_attachments.models import FileType +from unicef_attachments.serializers import BaseAttachmentSerializer + + +class CodedAttachmentSerializer(BaseAttachmentSerializer): + file_group = None + code = None + + file_type = FileTypeModelChoiceField( + queryset=FileType.objects.group_by(file_group), + label=_('Document Type'), + ) + + class Meta(BaseAttachmentSerializer.Meta): + fields = BaseAttachmentSerializer.Meta.fields + ['object_id'] + + def create(self, validated_data): + validated_data['code'] = self.code + return super().create(validated_data) diff --git a/src/etools/applications/attachments/views.py b/src/etools/applications/attachments/views.py new file mode 100644 index 0000000000..17b21023af --- /dev/null +++ b/src/etools/applications/attachments/views.py @@ -0,0 +1,26 @@ +from django.contrib.contenttypes.models import ContentType + +from rest_framework.viewsets import ModelViewSet +from unicef_attachments.models import Attachment +from unicef_restlib.views import NestedViewSetMixin, SafeTenantViewSetMixin + + +class CodedAttachmentViewSet(NestedViewSetMixin, SafeTenantViewSetMixin, ModelViewSet): + serializer_class = None + content_model = None + code = None + queryset = Attachment.objects.all() + + def get_parent_filter(self): + parent = self.get_parent_object() + if not parent: + return {} + + return { + 'code': self.code, + 'content_type_id': ContentType.objects.get_for_model(self.content_model).id, + 'object_id': parent.pk, + } + + def perform_create(self, serializer): + serializer.save(content_object=self.get_parent_object()) diff --git a/src/etools/applications/core/data/attachments_file_types.json b/src/etools/applications/core/data/attachments_file_types.json index dbb5a18a93..47dd308792 100644 --- a/src/etools/applications/core/data/attachments_file_types.json +++ b/src/etools/applications/core/data/attachments_file_types.json @@ -520,5 +520,16 @@ "code": "psea_answer", "group": ["psea_answer"] } + }, + { + "model": "unicef_attachments.filetype", + "pk": 62, + "fields": { + "order": 0, + "label": "PSEA NFR Attachment", + "name": "psea_nfr_attachment", + "code": "psea_nfr_attachment", + "group": ["psea"] + } } ] \ No newline at end of file diff --git a/src/etools/applications/psea/admin.py b/src/etools/applications/psea/admin.py index b9ca673c65..7e1c67d8a1 100644 --- a/src/etools/applications/psea/admin.py +++ b/src/etools/applications/psea/admin.py @@ -1,8 +1,15 @@ from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from etools.applications.partners.admin import AttachmentSingleInline from etools.applications.psea.models import Answer, Assessment, AssessmentActionPoint, Assessor, Evidence, Indicator +class NFRAttachmentInline(AttachmentSingleInline): + verbose_name_plural = _("NFR Attachment") + code = 'psea_nfr_attachment' + + @admin.register(Assessment) class AssessmentAdmin(admin.ModelAdmin): list_display = ('partner', 'get_status', 'overall_rating', ) @@ -14,6 +21,10 @@ def get_status(self, obj): return obj.status get_status.short_description = "Status" + inlines = ( + NFRAttachmentInline, + ) + @admin.register(Answer) class AnswerAdmin(admin.ModelAdmin): diff --git a/src/etools/applications/psea/models.py b/src/etools/applications/psea/models.py index 10cabeea15..5db7ab1e3c 100644 --- a/src/etools/applications/psea/models.py +++ b/src/etools/applications/psea/models.py @@ -1,6 +1,7 @@ from django.conf import settings from django.db import connection, models from django.db.models import Sum +from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -164,6 +165,12 @@ class Assessment(TimeStampedModel): verbose_name=_('UNICEF Focal Points'), related_name="pse_assessment_focal_point", ) + nfr_attachment = CodedGenericRelation( + Attachment, + verbose_name=_('NFR Attachment'), + code='psea_nfr_attachment', + blank=True, + ) class Meta: verbose_name = _('Assessment') @@ -264,6 +271,9 @@ def user_belongs(self, user): return self.user_is_assessor(user) def get_mail_context(self, user): + nfr_attachment = self.nfr_attachment.first() + if nfr_attachment: + nfr_attachment = settings.HOST + reverse('attachments:file', kwargs={'pk': nfr_attachment.pk}) context = { "partner_name": self.partner.name, "partner_vendor_number": self.partner.vendor_number, @@ -275,6 +285,7 @@ def get_mail_context(self, user): "assessment_ingo_reason": self.get_assessment_ingo_reason_display(), "assessor": str(self.assessor), "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in self.focal_points.all()), + "nfr_attachment": nfr_attachment } if self.status == self.STATUS_REJECTED: context["rejected_comment"] = self.get_rejected_comment() diff --git a/src/etools/applications/psea/notifications/assessment-final.py b/src/etools/applications/psea/notifications/assessment-final.py index 3043a469bc..c4301e61b4 100644 --- a/src/etools/applications/psea/notifications/assessment-final.py +++ b/src/etools/applications/psea/notifications/assessment-final.py @@ -20,6 +20,10 @@ UNICEF Focal Points: {{ focal_points }} + {% if nfr_attachment $} + NFR Attachment: {{ nfr_attachment }} + {% endif $} + Please update the Vendor Master Data in VISION accordingly Please note that this is an automated email and the mailbox is not monitored. Please do not reply to it. @@ -47,6 +51,10 @@ UNICEF Focal Points: {{ focal_points }}

+ {% if nfr_attachment $} + NFR Attachment: {{ nfr_attachment }}

+ {% endif $} + Please update the Vendor Master Data in VISION accordingly
Please note that this is an automated email and the mailbox is not monitored. Please do not reply to it. {% endblock %} diff --git a/src/etools/applications/psea/permission_matrix/assessment_permissions.csv b/src/etools/applications/psea/permission_matrix/assessment_permissions.csv index 7f69a31ada..87db74542a 100644 --- a/src/etools/applications/psea/permission_matrix/assessment_permissions.csv +++ b/src/etools/applications/psea/permission_matrix/assessment_permissions.csv @@ -25,3 +25,5 @@ Field no,Verbose Name,Field Name,Group,Condition,Status,Action,Allowed 1.4.4,Overall rating,overall_rating,,,submitted,Required,TRUE 1.4.5,Overall rating,overall_rating,,,final,Required,TRUE 1.5.0,Info Card,info_card,,,final,Edit,FALSE +1.8.0,NFR Attachment,nrf_attachment,UNICEF Audit Focal Point,,draft,Required,False +1.8.0,NFR Attachment,nrf_attachment,UNICEF Audit Focal Point,,draft,Edit,TRUE diff --git a/src/etools/applications/psea/serializers.py b/src/etools/applications/psea/serializers.py index 71ec4c69f6..1e18349693 100644 --- a/src/etools/applications/psea/serializers.py +++ b/src/etools/applications/psea/serializers.py @@ -7,11 +7,13 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from unicef_attachments.fields import FileTypeModelChoiceField +from unicef_attachments.fields import AttachmentSingleFileField, FileTypeModelChoiceField from unicef_attachments.models import Attachment, FileType +from unicef_attachments.serializers import AttachmentSerializerMixin from unicef_restlib.fields import SeparatedReadWriteField from etools.applications.action_points.serializers import ActionPointBaseSerializer, HistorySerializer +from etools.applications.attachments.serializers import CodedAttachmentSerializer from etools.applications.audit.models import UNICEFAuditFocalPoint from etools.applications.audit.purchase_order.models import PurchaseOrder from etools.applications.partners.serializers.partner_organization_v2 import ( @@ -58,7 +60,12 @@ def get_permissions(self, obj): return permissions.get_permissions() -class AssessmentSerializer(BaseAssessmentSerializer): +class NFRAttachmentSerializer(CodedAttachmentSerializer): + file_group = "psea" + code = 'psea_nfr_attachment' + + +class AssessmentSerializer(AttachmentSerializerMixin, BaseAssessmentSerializer): overall_rating = serializers.SerializerMethodField() assessor = serializers.SerializerMethodField() partner_name = serializers.CharField(source="partner.name", read_only=True) @@ -70,6 +77,7 @@ class AssessmentSerializer(BaseAssessmentSerializer): allow_null=True, required=False, ) + nfr_attachment = AttachmentSingleFileField() class Meta(BaseAssessmentSerializer.Meta): fields = '__all__' diff --git a/src/etools/applications/psea/tests/test_models.py b/src/etools/applications/psea/tests/test_models.py index 61bb939a55..88c86e9e2a 100644 --- a/src/etools/applications/psea/tests/test_models.py +++ b/src/etools/applications/psea/tests/test_models.py @@ -188,6 +188,7 @@ def test_get_mail_context(self): "partner_vendor_number": assessment.partner.vendor_number, "url": assessment.get_object_url(user=user), "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in assessment.focal_points.all()), + 'nfr_attachment': None, "overall_rating": assessment.overall_rating_display, "assessment_date": str(assessment.assessment_date), "assessor": str(assessment.assessor), @@ -236,6 +237,7 @@ def test_get_mail_context(self): "partner_vendor_number": assessment.partner.vendor_number, "url": assessment.get_object_url(user=user), "overall_rating": assessment.overall_rating_display, + 'nfr_attachment': None, "focal_points": ", ".join(f"{fp.get_full_name()} ({fp.email})" for fp in assessment.focal_points.all()), "assessment_date": str(assessment.assessment_date), "assessor": str(assessment.assessor), diff --git a/src/etools/applications/psea/urls.py b/src/etools/applications/psea/urls.py index 8ba152fd30..346982a0d5 100644 --- a/src/etools/applications/psea/urls.py +++ b/src/etools/applications/psea/urls.py @@ -17,6 +17,7 @@ assessor_api = NestedComplexRouter(root_api, r'assessment') assessor_api.register(r'assessor', views.AssessorViewSet, basename='assessor') +assessor_api.register(r'attachments', views.NFRAttachmentViewSet, basename='nfr-attachments') action_points_api = NestedComplexRouter( root_api, diff --git a/src/etools/applications/psea/views.py b/src/etools/applications/psea/views.py index 9a35844250..a076939f30 100644 --- a/src/etools/applications/psea/views.py +++ b/src/etools/applications/psea/views.py @@ -27,6 +27,7 @@ ActionPointAssigneeCondition, ActionPointAuthorCondition, ) +from etools.applications.attachments.views import CodedAttachmentViewSet from etools.applications.audit.models import UNICEFAuditFocalPoint from etools.applications.partners.views.v2 import choices_to_json_ready from etools.applications.permissions2.conditions import ObjectStatusCondition @@ -50,6 +51,7 @@ AssessmentStatusSerializer, AssessorSerializer, IndicatorSerializer, + NFRAttachmentSerializer, ) from etools.applications.psea.validation import AssessmentValid @@ -241,7 +243,7 @@ def update(self, request, *args, **kwargs): ).data ) - def _set_status(self, request, assessment_status): + def _set_status(self, request, assessment_status, **kwargs): self.serializer_class = AssessmentStatusSerializer status = { "status": assessment_status, @@ -250,6 +252,8 @@ def _set_status(self, request, assessment_status): if comment: status["comment"] = comment request.data.clear() + if 'nfr_attachment' in kwargs: + request.data.update({"nfr_attachment": kwargs.get('nfr_attachment')}) request.data.update({"status": assessment_status}) request.data.update( {"status_history": [status]}, @@ -270,7 +274,7 @@ def submit(self, request, pk=None): @action(detail=True, methods=["patch"]) def finalize(self, request, pk=None): - return self._set_status(request, Assessment.STATUS_FINAL) + return self._set_status(request, Assessment.STATUS_FINAL, **request.data) @action(detail=True, methods=["patch"]) def cancel(self, request, pk=None): @@ -472,6 +476,12 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) +class NFRAttachmentViewSet(CodedAttachmentViewSet): + serializer_class = NFRAttachmentSerializer + content_model = Assessment + code = 'nfr_attachment' + + class IndicatorViewSet( SafeTenantViewSetMixin, mixins.ListModelMixin, diff --git a/src/etools/applications/tpm/admin.py b/src/etools/applications/tpm/admin.py index dcc6a87eb5..3370261686 100644 --- a/src/etools/applications/tpm/admin.py +++ b/src/etools/applications/tpm/admin.py @@ -17,15 +17,19 @@ class TPMActivityAdmin(admin.ModelAdmin): 'tpm_visit__pk' ) filter_horizontal = ( - 'locations', 'unicef_focal_points', 'offices', ) - raw_id_fields = ('partner', 'intervention', 'cp_output') + raw_id_fields = ('tpm_visit', 'partner', 'intervention', 'cp_output', 'unicef_focal_points', 'locations') def visit(self, obj): return obj.tpm_visit.reference_number + # inlines = ( + # 'attachments', + # 'report_attachments' + # ) + class ActivityInline(admin.StackedInline): model = models.TPMActivity @@ -47,15 +51,19 @@ class TPMVisitAdmin(admin.ModelAdmin): readonly_fields = ('status', 'reference_number') list_display = ('reference_number', 'tpm_partner', 'status', 'visit_information') list_filter = ('status', ) - filter_horizontal = ('tpm_partner_focal_points', ) inlines = [ActivityInline] - raw_id_fields = ('author', 'tpm_partner', ) + raw_id_fields = ('author', 'tpm_partner', 'tpm_partner_focal_points') custom_fields = ['is_deleted', 'reference_number'] search_fields = ['tpm_partner__name', 'pk'] def reference_number(self, obj): return obj.reference_number + # inlines = ( + # report_attachments + # attachments + # ) + @admin.register(models.TPMActionPoint) class TPMActionPointAdmin(admin.ModelAdmin): From 8a7c4b080672c71cc33e66fb781b5841cbf5a605 Mon Sep 17 00:00:00 2001 From: Domenico Date: Thu, 17 Jun 2021 17:28:03 -0500 Subject: [PATCH 22/26] 25833 fix psea ap notification --- .../psea/notifications/assessment-action_point_assigned.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py index 4d55dd7e0d..1c7760ab77 100644 --- a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py +++ b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py @@ -12,7 +12,7 @@ PSEA Assessment Reference Number: {{ action_point.reference_number }} Due Date: {{ action_point.due_date }} - Link: {{ action_point.psea_assessment.url }} + Link: {{ action_point.object_url }} Thank you. """), @@ -27,7 +27,7 @@ PSEA Assessment Reference Number: {{ action_point.reference_number }}
Due Date: {{ action_point.due_date }}
- Link: click here

+ Link: click here

Thank you. {% endblock %} From 02904d0766a88c6fddf5567e064598c3f0d2b527 Mon Sep 17 00:00:00 2001 From: Domenico Date: Fri, 18 Jun 2021 09:31:04 -0500 Subject: [PATCH 23/26] remove attachment generic class --- .../applications/attachments/serializers.py | 22 ---------------- src/etools/applications/attachments/views.py | 26 ------------------- src/etools/applications/psea/serializers.py | 6 ----- src/etools/applications/psea/urls.py | 1 - src/etools/applications/psea/views.py | 8 ------ 5 files changed, 63 deletions(-) delete mode 100644 src/etools/applications/attachments/serializers.py delete mode 100644 src/etools/applications/attachments/views.py diff --git a/src/etools/applications/attachments/serializers.py b/src/etools/applications/attachments/serializers.py deleted file mode 100644 index a61de9bcc0..0000000000 --- a/src/etools/applications/attachments/serializers.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -from unicef_attachments.fields import FileTypeModelChoiceField -from unicef_attachments.models import FileType -from unicef_attachments.serializers import BaseAttachmentSerializer - - -class CodedAttachmentSerializer(BaseAttachmentSerializer): - file_group = None - code = None - - file_type = FileTypeModelChoiceField( - queryset=FileType.objects.group_by(file_group), - label=_('Document Type'), - ) - - class Meta(BaseAttachmentSerializer.Meta): - fields = BaseAttachmentSerializer.Meta.fields + ['object_id'] - - def create(self, validated_data): - validated_data['code'] = self.code - return super().create(validated_data) diff --git a/src/etools/applications/attachments/views.py b/src/etools/applications/attachments/views.py deleted file mode 100644 index 17b21023af..0000000000 --- a/src/etools/applications/attachments/views.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.contrib.contenttypes.models import ContentType - -from rest_framework.viewsets import ModelViewSet -from unicef_attachments.models import Attachment -from unicef_restlib.views import NestedViewSetMixin, SafeTenantViewSetMixin - - -class CodedAttachmentViewSet(NestedViewSetMixin, SafeTenantViewSetMixin, ModelViewSet): - serializer_class = None - content_model = None - code = None - queryset = Attachment.objects.all() - - def get_parent_filter(self): - parent = self.get_parent_object() - if not parent: - return {} - - return { - 'code': self.code, - 'content_type_id': ContentType.objects.get_for_model(self.content_model).id, - 'object_id': parent.pk, - } - - def perform_create(self, serializer): - serializer.save(content_object=self.get_parent_object()) diff --git a/src/etools/applications/psea/serializers.py b/src/etools/applications/psea/serializers.py index 1e18349693..d6f88c8d3b 100644 --- a/src/etools/applications/psea/serializers.py +++ b/src/etools/applications/psea/serializers.py @@ -13,7 +13,6 @@ from unicef_restlib.fields import SeparatedReadWriteField from etools.applications.action_points.serializers import ActionPointBaseSerializer, HistorySerializer -from etools.applications.attachments.serializers import CodedAttachmentSerializer from etools.applications.audit.models import UNICEFAuditFocalPoint from etools.applications.audit.purchase_order.models import PurchaseOrder from etools.applications.partners.serializers.partner_organization_v2 import ( @@ -60,11 +59,6 @@ def get_permissions(self, obj): return permissions.get_permissions() -class NFRAttachmentSerializer(CodedAttachmentSerializer): - file_group = "psea" - code = 'psea_nfr_attachment' - - class AssessmentSerializer(AttachmentSerializerMixin, BaseAssessmentSerializer): overall_rating = serializers.SerializerMethodField() assessor = serializers.SerializerMethodField() diff --git a/src/etools/applications/psea/urls.py b/src/etools/applications/psea/urls.py index 346982a0d5..8ba152fd30 100644 --- a/src/etools/applications/psea/urls.py +++ b/src/etools/applications/psea/urls.py @@ -17,7 +17,6 @@ assessor_api = NestedComplexRouter(root_api, r'assessment') assessor_api.register(r'assessor', views.AssessorViewSet, basename='assessor') -assessor_api.register(r'attachments', views.NFRAttachmentViewSet, basename='nfr-attachments') action_points_api = NestedComplexRouter( root_api, diff --git a/src/etools/applications/psea/views.py b/src/etools/applications/psea/views.py index a076939f30..9860812ee0 100644 --- a/src/etools/applications/psea/views.py +++ b/src/etools/applications/psea/views.py @@ -27,7 +27,6 @@ ActionPointAssigneeCondition, ActionPointAuthorCondition, ) -from etools.applications.attachments.views import CodedAttachmentViewSet from etools.applications.audit.models import UNICEFAuditFocalPoint from etools.applications.partners.views.v2 import choices_to_json_ready from etools.applications.permissions2.conditions import ObjectStatusCondition @@ -51,7 +50,6 @@ AssessmentStatusSerializer, AssessorSerializer, IndicatorSerializer, - NFRAttachmentSerializer, ) from etools.applications.psea.validation import AssessmentValid @@ -476,12 +474,6 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) -class NFRAttachmentViewSet(CodedAttachmentViewSet): - serializer_class = NFRAttachmentSerializer - content_model = Assessment - code = 'nfr_attachment' - - class IndicatorViewSet( SafeTenantViewSetMixin, mixins.ListModelMixin, From d903345d52e9fa7771d7e34bb2a4e8383233ba9c Mon Sep 17 00:00:00 2001 From: Domenico Date: Fri, 25 Jun 2021 12:05:56 -0500 Subject: [PATCH 24/26] 25833 fix psea notification --- .../notifications/assessment-action_point_assigned.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py index 1c7760ab77..c69c4ac590 100644 --- a/src/etools/applications/psea/notifications/assessment-action_point_assigned.py +++ b/src/etools/applications/psea/notifications/assessment-action_point_assigned.py @@ -3,14 +3,14 @@ name = 'psea/assessment/action_point_assigned' defaults = { 'description': 'PSEA Assessment action point was assigned', - 'subject': '[eTools] ACTION POINT ASSIGNED to {{ action_point.visit_lead }}', + 'subject': '[eTools] ACTION POINT ASSIGNED to {{ action_point.person_responsible }}', 'content': strip_text(""" - Dear {{ action_point.visit_lead }}, + Dear {{ action_point.person_responsible }}, {{ action_point.assigned_by }} has assigned you an action point. - PSEA Assessment Reference Number: {{ action_point.reference_number }} + PSEA Assessment Reference Number: {{ action_point.psea_assessment.reference_number }} Due Date: {{ action_point.due_date }} Link: {{ action_point.object_url }} @@ -21,11 +21,11 @@ {% extends "email-templates/base" %} {% block content %} - Dear {{ action_point.visit_lead }},

+ Dear {{ action_point.person_responsible }},

{{ action_point.assigned_by }} has assigned you an action point.

- PSEA Assessment Reference Number: {{ action_point.reference_number }}
+ PSEA Assessment Reference Number: {{ action_point.psea_assessment.reference_number }}
Due Date: {{ action_point.due_date }}
Link: click here

From 4296d5b98e0ed6c5de665ee6ee2f212bd5b448c6 Mon Sep 17 00:00:00 2001 From: Domenico Date: Tue, 29 Jun 2021 18:50:53 -0500 Subject: [PATCH 25/26] 26185 fix inactive indicator --- .../applications/partners/serializers/interventions_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index d9b969faf3..6b79a12a25 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -329,7 +329,7 @@ class InterventionResultNestedSerializer(serializers.ModelSerializer): ll_results = LowerResultSerializer(many=True, read_only=True) def get_ram_indicator_names(self, obj): - return [i.name for i in obj.ram_indicators.all()] + return [i.light_repr for i in obj.ram_indicators.all()] class Meta: model = InterventionResultLink From 5469393d186ff54214d1216186f7ed60096f4bbb Mon Sep 17 00:00:00 2001 From: Roman Karpovich Date: Wed, 30 Jun 2021 12:52:31 +0300 Subject: [PATCH 26/26] capture offline backend communication failures with sentry --- .../data_collection/offline/synchronizer.py | 49 ++++++++++------- .../data_collection/tests/test_offline.py | 55 +++++++++++++++++++ 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py b/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py index 4dea7d198a..06377d2067 100644 --- a/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py +++ b/src/etools/applications/field_monitoring/data_collection/offline/synchronizer.py @@ -7,6 +7,7 @@ from django.urls import reverse from etools_offline import OfflineCollect +from sentry_sdk import capture_exception from simplejson import JSONDecodeError from etools.applications.environment.helpers import tenant_switch_is_active @@ -46,33 +47,39 @@ def initialize_blueprints(self) -> None: for method in self.activity.methods: blueprint = get_blueprint_for_activity_and_method(self.activity, method) - OfflineCollect().add(data={ - "is_active": True, - "code": blueprint.code, - "form_title": blueprint.title, - "form_instructions": json.dumps(blueprint.to_dict(), indent=2), - "accessible_by": self._get_data_collectors(), - "api_response_url": urljoin( - host, - '{}?workspace={}'.format( - reverse( - 'field_monitoring_data_collection:activities-offline', - args=[self.activity.id, method.id] - ), - connection.tenant.schema_name or '' + try: + OfflineCollect().add(data={ + "is_active": True, + "code": blueprint.code, + "form_title": blueprint.title, + "form_instructions": json.dumps(blueprint.to_dict(), indent=2), + "accessible_by": self._get_data_collectors(), + "api_response_url": urljoin( + host, + '{}?workspace={}'.format( + reverse( + 'field_monitoring_data_collection:activities-offline', + args=[self.activity.id, method.id] + ), + connection.tenant.schema_name or '' + ) ) - ) - }) + }) + except JSONDecodeError: + capture_exception() def update_data_collectors_list(self) -> None: if not self.enabled: return for method in self.activity.methods: - OfflineCollect().update( - get_blueprint_code(self.activity, method), - accessible_by=self._get_data_collectors() - ) + try: + OfflineCollect().update( + get_blueprint_code(self.activity, method), + accessible_by=self._get_data_collectors() + ) + except JSONDecodeError: + capture_exception() def close_blueprints(self) -> None: if not self.enabled: @@ -85,4 +92,4 @@ def close_blueprints(self) -> None: try: OfflineCollect().delete(code) except JSONDecodeError: - pass + capture_exception() diff --git a/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py b/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py index 45dc842480..42b89ffa65 100644 --- a/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py +++ b/src/etools/applications/field_monitoring/data_collection/tests/test_offline.py @@ -2,6 +2,7 @@ from django.db import connection from django.test import override_settings +import simplejson from mock import patch from rest_framework import status @@ -280,6 +281,60 @@ def test_tenant_switch_missing_but_api_not_configured(self, add_mock): self._test_update(self.fm_user, activity, {'status': 'assigned'}) add_mock.assert_not_called() + @override_settings(ETOOLS_OFFLINE_API='http://example.com/b/api/remote/blueprint/', SENTRY_DSN='https://test.dns') + @patch('sentry_sdk.api.Hub.current.capture_exception') + @patch('etools.applications.field_monitoring.data_collection.offline.synchronizer.OfflineCollect.add') + def test_add_offline_backend_unavailable(self, add_mock, capture_event_mock): + def communication_failure(*args, **kwargs): + return 502, simplejson.loads( + '\r\n502 Bad Gateway\r\n\r\n
' + '

502 Bad Gateway

\r\n
nginx/1.13.12
\r\n\r\n\r\n' + ) + + add_mock.side_effect = communication_failure + + activity = MonitoringActivityFactory(status='pre_assigned', partners=[PartnerFactory()]) + ActivityQuestionFactory(monitoring_activity=activity, is_enabled=True, question__methods=[MethodFactory()]) + + self._test_update(self.fm_user, activity, {'status': 'assigned'}) + capture_event_mock.assert_called() + + @override_settings(ETOOLS_OFFLINE_API='http://example.com/b/api/remote/blueprint/', SENTRY_DSN='https://test.dns') + @patch('sentry_sdk.api.Hub.current.capture_exception') + @patch('etools.applications.field_monitoring.data_collection.offline.synchronizer.OfflineCollect.update') + def test_update_offline_backend_unavailable(self, update_mock, capture_event_mock): + def communication_failure(*args, **kwargs): + return 502, simplejson.loads( + '\r\n502 Bad Gateway\r\n\r\n
' + '

502 Bad Gateway

\r\n
nginx/1.13.12
\r\n\r\n\r\n' + ) + + update_mock.side_effect = communication_failure + + activity = MonitoringActivityFactory(status='data_collection', partners=[PartnerFactory()]) + ActivityQuestionFactory(monitoring_activity=activity, is_enabled=True, question__methods=[MethodFactory()]) + + activity.team_members.remove(activity.team_members.first()) + capture_event_mock.assert_called() + + @override_settings(ETOOLS_OFFLINE_API='http://example.com/b/api/remote/blueprint/', SENTRY_DSN='https://test.dns') + @patch('sentry_sdk.api.Hub.current.capture_exception') + @patch('etools.applications.field_monitoring.data_collection.offline.synchronizer.OfflineCollect.delete') + def test_delete_offline_backend_unavailable(self, delete_mock, capture_event_mock): + def communication_failure(*args, **kwargs): + return 502, simplejson.loads( + '\r\n502 Bad Gateway\r\n\r\n
' + '

502 Bad Gateway

\r\n
nginx/1.13.12
\r\n\r\n\r\n' + ) + + delete_mock.side_effect = communication_failure + + activity = MonitoringActivityFactory(status='data_collection', partners=[PartnerFactory()]) + ActivityQuestionFactory(monitoring_activity=activity, is_enabled=True, question__methods=[MethodFactory()]) + + self._test_update(self.fm_user, activity, {'status': 'cancelled', 'cancel_reason': 'For testing purposes'}) + capture_event_mock.assert_called() + class MonitoringActivityOfflineValuesTestCase(APIViewSetTestCase, BaseTenantTestCase): base_view = 'field_monitoring_data_collection:activities'