From 6fc5b060338c462c8060ec6eea2194f09bc8d60a Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 28 Feb 2019 14:51:34 -0800 Subject: [PATCH 001/817] feat: Added .strip() after lowercasing email address from AD response [#1077] --- django_api/django_api/apps/core/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_api/django_api/apps/core/mixins.py b/django_api/django_api/apps/core/mixins.py index 2d32e5c6e..69759c89a 100644 --- a/django_api/django_api/apps/core/mixins.py +++ b/django_api/django_api/apps/core/mixins.py @@ -28,7 +28,7 @@ def social_details(backend, details, response, *args, **kwargs): email = r['details'].get('email') if isinstance(email, str): - r['details']['email'] = email.lower() + r['details']['email'] = email.lower().strip() return r From c8a67d8287028561e513c20f59b03fc2560fd0f8 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 28 Feb 2019 15:24:55 -0800 Subject: [PATCH 002/817] fix: Disabled create_user pipeline function in python-social-auth settings [#1078] --- django_api/django_api/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_api/django_api/settings/base.py b/django_api/django_api/settings/base.py index 942af36cb..1e61d2a10 100644 --- a/django_api/django_api/settings/base.py +++ b/django_api/django_api/settings/base.py @@ -445,7 +445,7 @@ # 'social_core.pipeline.user.get_username', 'core.mixins.get_username', 'social_core.pipeline.social_auth.associate_by_email', - 'social_core.pipeline.user.create_user', + # 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', # 'social_core.pipeline.user.user_details', From 07b0d20dc944d2e133cd9ba686f45f2cd32ff037 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 1 Mar 2019 11:18:05 -0800 Subject: [PATCH 003/817] refactor: Email template updates for progress report status changes [#1075] --- ..._progress_report_status_change_accepted.html | 15 +++++++++++++-- ...progress_report_status_change_sent_back.html | 17 +++++++++++++++-- ...on_progress_report_status_change_subject.txt | 2 +- ...progress_report_status_change_submitted.html | 15 +++++++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html b/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html index f0e6ff5d4..8d89ec7c2 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html @@ -2,6 +2,17 @@ {% block content %}

Dear {{ person.name }},

-

Your progress report {{ pr_url }} for {{ pd_ref_title }}} has been Accepted by UNICEF.

-

Best Regards,

+

Please note that the following was accepted by UNICEF:

+
+

Report: [{{pr.report_type}}{{pr.report_number}}]

+

Period: {{pr.start_date}} - {{pr.end_date}}

+

Submission Date: {{pr.submission_date}}

+

Due Date: {{pr.due_date}}

+

Link to the report: {{ pr_url }}

+

Programme Document Ref: {{pr.pd.reference_number}}

+

Programme Document Title: {{pr.pd.title}}

+
+

Kind regards,

+

UNICEF

+

-- Please note that replies to this email are not monitored and cannot be replied to --

{% endblock content %} \ No newline at end of file diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html b/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html index 44fc32cd3..4fc130d76 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html @@ -2,6 +2,19 @@ {% block content %}

Dear {{ person.name }},

-

Your progress report {{ pr_url }} for {{ pd_ref_title }}} has been Sent Back for Revision by UNICEF. Please make the necessary changes and resubmit this report.

-

Best Regards,

+

Please note that the following was sent back by UNICEF:

+
+

Report: [{{pr.report_type}}{{pr.report_number}}]

+

Period: {{pr.start_date}} - {{pr.end_date}}

+

Submission Date: {{pr.submission_date}}

+

Due Date: {{pr.due_date}}

+

Link to the report: {{ pr_url }}

+

Programme Document Ref: {{pr.pd.reference_number}}

+

Programme Document Title: {{pr.pd.title}}

+
+

Please log in to the UNICEF Partner Reporting Portal to see the reason and contact details of the person that sent the report back.

+
+

Kind regards,

+

UNICEF

+

-- Please note that replies to this email are not monitored and cannot be replied to --

{% endblock content %} \ No newline at end of file diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_subject.txt b/django_api/django_api/templates/emails/on_progress_report_status_change_subject.txt index 1230bfb93..58bf8f5b7 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_subject.txt +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_subject.txt @@ -1 +1 @@ -[Partner Reporting Portal] Progress Report {{ status }} \ No newline at end of file +[UNICEF Partner Reporting Portal] Report {{ status }} \ No newline at end of file diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html b/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html index c412f5b40..7a948cf0a 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html @@ -2,6 +2,17 @@ {% block content %}

Dear {{ person.name }},

-

Your progress report {{ pr_url }} for {{ pd_ref_title }}} has been submitted.

-

Best Regards,

+

Please note that the following was submitted to UNICEF:

+
+

Report: [{{pr.report_type}}{{pr.report_number}}]

+

Period: {{pr.start_date}} - {{pr.end_date}}

+

Submission Date: {{pr.submission_date}}

+

Due Date: {{pr.due_date}}

+

Link to the report: {{ pr_url }}

+

Programme Document Ref: {{pr.pd.reference_number}}

+

Programme Document Title: {{pr.pd.title}}

+
+

Kind regards,

+

UNICEF

+

-- Please note that replies to this email are not monitored and cannot be replied to --

{% endblock content %} \ No newline at end of file From c55e4c6ebf2ba2309f6097e16a6b07ed03938d3e Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 1 Mar 2019 11:50:22 -0800 Subject: [PATCH 004/817] feat: Modified send_notification_on_status_change post_signal for ProgressReport to send emails to submitted_by, submitting_user, and unicef_focal_point [#1075] --- django_api/django_api/apps/unicef/models.py | 26 +++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/django_api/django_api/apps/unicef/models.py b/django_api/django_api/apps/unicef/models.py index 8356f894b..19efd6060 100644 --- a/django_api/django_api/apps/unicef/models.py +++ b/django_api/django_api/apps/unicef/models.py @@ -480,12 +480,34 @@ def send_notification_on_status_change(sender, instance, **kwargs): template_data = { 'person': None, + 'pr': instance, 'pr_url': pr_url, - 'pd_ref_title': f'{pd.reference_number} ({pd.title})', 'status': instance.get_status_display() } - for person in pd.unicef_officers.all(): + template_data['person'] = instance.submitted_by + to_email_list = [template_data['person'].email] + + send_email_from_template( + subject_template_path=subject_template_path, + body_template_path=body_template_path, + template_data=template_data, + to_email_list=to_email_list, + content_subtype='html', + ) + + template_data['person'] = instance.submitting_user + to_email_list = [template_data['person'].email] + + send_email_from_template( + subject_template_path=subject_template_path, + body_template_path=body_template_path, + template_data=template_data, + to_email_list=to_email_list, + content_subtype='html', + ) + + for person in pd.unicef_focal_point.all(): template_data['person'] = person to_email_list = [person.email] From e10475e55f7ac41a1b0828e771ed681bb46b8580 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 1 Mar 2019 15:11:08 -0800 Subject: [PATCH 005/817] fix: Updated template data for progress report status change emails [#1075] + Added an email check for submitting_user against submitted_by not to duplicate email notifications --- django_api/django_api/apps/unicef/models.py | 24 ++++++++++--------- ...rogress_report_status_change_accepted.html | 12 +++++----- ...ogress_report_status_change_sent_back.html | 12 +++++----- ...ogress_report_status_change_submitted.html | 12 +++++----- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/django_api/django_api/apps/unicef/models.py b/django_api/django_api/apps/unicef/models.py index 19efd6060..785db7aed 100644 --- a/django_api/django_api/apps/unicef/models.py +++ b/django_api/django_api/apps/unicef/models.py @@ -480,12 +480,13 @@ def send_notification_on_status_change(sender, instance, **kwargs): template_data = { 'person': None, - 'pr': instance, + 'report': instance, + 'pd': pd, 'pr_url': pr_url, 'status': instance.get_status_display() } - template_data['person'] = instance.submitted_by + template_data['person'] = instance.submitting_user to_email_list = [template_data['person'].email] send_email_from_template( @@ -496,16 +497,17 @@ def send_notification_on_status_change(sender, instance, **kwargs): content_subtype='html', ) - template_data['person'] = instance.submitting_user - to_email_list = [template_data['person'].email] + if instance.submitted_by.email != instance.submitting_user.email: + template_data['person'] = instance.submitted_by + to_email_list = [template_data['person'].email] - send_email_from_template( - subject_template_path=subject_template_path, - body_template_path=body_template_path, - template_data=template_data, - to_email_list=to_email_list, - content_subtype='html', - ) + send_email_from_template( + subject_template_path=subject_template_path, + body_template_path=body_template_path, + template_data=template_data, + to_email_list=to_email_list, + content_subtype='html', + ) for person in pd.unicef_focal_point.all(): template_data['person'] = person diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html b/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html index 8d89ec7c2..47ba098dc 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_accepted.html @@ -4,13 +4,13 @@

Dear {{ person.name }},

Please note that the following was accepted by UNICEF:


-

Report: [{{pr.report_type}}{{pr.report_number}}]

-

Period: {{pr.start_date}} - {{pr.end_date}}

-

Submission Date: {{pr.submission_date}}

-

Due Date: {{pr.due_date}}

+

Report: {{report.report_type}}{{report.report_number}}

+

Period: {{report.start_date}} - {{report.end_date}}

+

Submission Date: {{report.submission_date}}

+

Due Date: {{report.due_date}}

Link to the report: {{ pr_url }}

-

Programme Document Ref: {{pr.pd.reference_number}}

-

Programme Document Title: {{pr.pd.title}}

+

Programme Document Ref: {{pd.reference_number}}

+

Programme Document Title: {{pd.title}}


Kind regards,

UNICEF

diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html b/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html index 4fc130d76..c368b3a37 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_sent_back.html @@ -4,13 +4,13 @@

Dear {{ person.name }},

Please note that the following was sent back by UNICEF:


-

Report: [{{pr.report_type}}{{pr.report_number}}]

-

Period: {{pr.start_date}} - {{pr.end_date}}

-

Submission Date: {{pr.submission_date}}

-

Due Date: {{pr.due_date}}

+

Report: {{report.report_type}}{{report.report_number}}

+

Period: {{report.start_date}} - {{report.end_date}}

+

Submission Date: {{report.submission_date}}

+

Due Date: {{report.due_date}}

Link to the report: {{ pr_url }}

-

Programme Document Ref: {{pr.pd.reference_number}}

-

Programme Document Title: {{pr.pd.title}}

+

Programme Document Ref: {{pd.reference_number}}

+

Programme Document Title: {{pd.title}}


Please log in to the UNICEF Partner Reporting Portal to see the reason and contact details of the person that sent the report back.


diff --git a/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html b/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html index 7a948cf0a..e832e1c6c 100644 --- a/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html +++ b/django_api/django_api/templates/emails/on_progress_report_status_change_submitted.html @@ -4,13 +4,13 @@

Dear {{ person.name }},

Please note that the following was submitted to UNICEF:


-

Report: [{{pr.report_type}}{{pr.report_number}}]

-

Period: {{pr.start_date}} - {{pr.end_date}}

-

Submission Date: {{pr.submission_date}}

-

Due Date: {{pr.due_date}}

+

Report: {{report.report_type}}{{report.report_number}}

+

Period: {{report.start_date}} - {{report.end_date}}

+

Submission Date: {{report.submission_date}}

+

Due Date: {{report.due_date}}

Link to the report: {{ pr_url }}

-

Programme Document Ref: {{pr.pd.reference_number}}

-

Programme Document Title: {{pr.pd.title}}

+

Programme Document Ref: {{pd.reference_number}}

+

Programme Document Title: {{pd.title}}


Kind regards,

UNICEF

From 7254225f6d6e3c0aad9477f57239eedd40765de9 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 1 Mar 2019 15:42:50 -0800 Subject: [PATCH 006/817] feat: Created 2 celery task functions for sending email notification on due and overdue progress reports [#1074] + notify_ip_due_reports + notify_ip_overdue_reports + send_due_progress_report_email + send_overdue_progress_report_email + email templates --- django_api/django_api/apps/indicator/tasks.py | 49 ++++++++++++-- django_api/django_api/apps/utils/emails.py | 66 ++++++++++++++++++- .../templates/emails/due_progress_report.html | 16 +++++ .../emails/due_progress_report_subject.txt | 1 + .../emails/overdue_progress_report.html | 16 +++++ .../overdue_progress_report_subject.txt | 1 + 6 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 django_api/django_api/templates/emails/due_progress_report.html create mode 100644 django_api/django_api/templates/emails/due_progress_report_subject.txt create mode 100644 django_api/django_api/templates/emails/overdue_progress_report.html create mode 100644 django_api/django_api/templates/emails/overdue_progress_report_subject.txt diff --git a/django_api/django_api/apps/indicator/tasks.py b/django_api/django_api/apps/indicator/tasks.py index b7d07030f..c693a4aef 100644 --- a/django_api/django_api/apps/indicator/tasks.py +++ b/django_api/django_api/apps/indicator/tasks.py @@ -1,11 +1,16 @@ +import logging +from datetime import datetime, date, timedelta +from dateutil.relativedelta import relativedelta + from celery import shared_task from indicator.models import IndicatorReport from unicef.models import ProgressReport from core.common import INDICATOR_REPORT_STATUS, PROGRESS_REPORT_STATUS +from utils.emails import send_due_progress_report_email, send_overdue_progress_report_email -from datetime import datetime, timedelta +logger = logging.getLogger(__name__) OVERDUE_DAYS = 15 @@ -13,12 +18,12 @@ def process_due_reports(): updates = list() today = datetime.now().date() - print("Create due/overdue indicator reports") + logger.info("Create due/overdue indicator reports") # Get all open (without submission date) indicator reports reports = IndicatorReport.objects.filter(submission_date__isnull=True) # Iterate and set proper status for report in reports: - print("Indicator Report: %s" % report.id) + logger.info("Indicator Report: %s" % report.id) due_date = report.due_date or report.time_period_end + timedelta(days=OVERDUE_DAYS) if due_date < today and report.report_status != INDICATOR_REPORT_STATUS.overdue: report.report_status = INDICATOR_REPORT_STATUS.overdue @@ -32,7 +37,7 @@ def process_due_reports(): reports = ProgressReport.objects.filter(submission_date__isnull=True) # Iterate and set proper status for report in reports: - print("Progress Report: %s" % report.id) + logger.info("Progress Report: %s" % report.id) due_date = report.due_date or report.end_date + timedelta(days=OVERDUE_DAYS) if due_date < today and report.status != PROGRESS_REPORT_STATUS.overdue: report.status = PROGRESS_REPORT_STATUS.overdue @@ -47,3 +52,39 @@ def process_due_reports(): return "Updated %s Reports: %s" % (len(updates), ", ".join(["(%s for ID %d)" % ( status, report.id) for status, report in updates])) if updates else "---" + + +@shared_task +def notify_ip_due_reports(): + logger.info("Notifying IP due progress reports") + notified = list() + + today = date.today() + unsubmitted_due_reports = ProgressReport.objects.filter( + submission_date__isnull=True, + status=PROGRESS_REPORT_STATUS.due, + due_date=today + relativedelta(days=7), + ) + + for report in unsubmitted_due_reports: + send_due_progress_report_email(report) + notified.append(report.id) + + return "Sent emails for %s Due Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" + + +@shared_task +def notify_ip_overdue_reports(): + logger.info("Notifying IP overdue progress reports") + notified = list() + + unsubmitted_overdue_reports = ProgressReport.objects.filter( + submission_date__isnull=True, + status=PROGRESS_REPORT_STATUS.overdue, + ) + + for report in unsubmitted_overdue_reports: + send_overdue_progress_report_email(report) + notified.append(report.id) + + return "Sent emails for %s Overdue Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" diff --git a/django_api/django_api/apps/utils/emails.py b/django_api/django_api/apps/utils/emails.py index 9f677d572..c14cda79c 100644 --- a/django_api/django_api/apps/utils/emails.py +++ b/django_api/django_api/apps/utils/emails.py @@ -1,6 +1,6 @@ from django.conf import settings from django.core.mail import EmailMultiAlternatives -from django.template.loader import get_template +from django.template.loader import render_to_string def send_email_from_template( @@ -29,11 +29,71 @@ def send_email_from_template( """ message = EmailMultiAlternatives( - get_template(subject_template_path).render(template_data).strip(), - get_template(body_template_path).render(template_data), + render_to_string(subject_template_path, template_data).strip(), + render_to_string(body_template_path, template_data), from_email, to_email_list, **kwargs ) message.content_subtype = content_subtype message.send(fail_silently=fail_silently) + + +def send_due_progress_report_email(progress_report): + """send_due_progress_report_email sends email notifications to + UNICEF Authorized Officers and Focal Points about reports due in 1 week. + + Arguments: + progress_report {ProgressReport} -- ProgressReport ORM object to send email notification about + """ + + pd = progress_report.programme_document + + template_data = { + 'person': None, + 'progress_report': progress_report, + 'programme_document': pd, + } + + to_emails = set() + to_emails |= set(map(lambda person: person, pd.unicef_officers.all())) + to_emails |= set(map(lambda person: person, pd.unicef_focal_point.all())) + + for person in to_emails: + send_email_from_template( + subject_template_path='emails/due_progress_report_subject.txt', + body_template_path='emails/due_progress_report.html', + template_data=template_data, + to_email_list=[person.email, ], + content_subtype='html' + ) + + +def send_overdue_progress_report_email(progress_report): + """send_overdue_progress_report_email sends email notifications to + UNICEF Authorized Officers and Focal Points about overdue reports. + + Arguments: + progress_report {ProgressReport} -- ProgressReport ORM object to send email notification about + """ + + pd = progress_report.programme_document + + template_data = { + 'person': None, + 'progress_report': progress_report, + 'programme_document': pd, + } + + to_emails = set() + to_emails |= set(map(lambda person: person, pd.unicef_officers.all())) + to_emails |= set(map(lambda person: person, pd.unicef_focal_point.all())) + + for person in to_emails: + send_email_from_template( + subject_template_path='emails/overdue_progress_report_subject.txt', + body_template_path='emails/overdue_progress_report.html', + template_data=template_data, + to_email_list=[person.email, ], + content_subtype='html' + ) diff --git a/django_api/django_api/templates/emails/due_progress_report.html b/django_api/django_api/templates/emails/due_progress_report.html new file mode 100644 index 000000000..5a2a54914 --- /dev/null +++ b/django_api/django_api/templates/emails/due_progress_report.html @@ -0,0 +1,16 @@ +{% extends 'emails/base.html' %} + +{% block content %} +

Dear {{ person.name }},

+

Please note that the following is due in 1 week:

+
+

Report: {{progress_report.report_type}}{{progress_report.report_number}}

+

Period: {{progress_report.start_date}} - {{progress_report.end_date}}

+

Due Date: {{progress_report.due_date}}

+

Programme Document Ref: {{programme_document.reference_number}}

+

Programme Document Title: {{programme_document.title}}

+
+

Kind regards,

+

UNICEF

+

-- Please note that replies to this email are not monitored and cannot be replied to --

+{% endblock content %} \ No newline at end of file diff --git a/django_api/django_api/templates/emails/due_progress_report_subject.txt b/django_api/django_api/templates/emails/due_progress_report_subject.txt new file mode 100644 index 000000000..14185d055 --- /dev/null +++ b/django_api/django_api/templates/emails/due_progress_report_subject.txt @@ -0,0 +1 @@ +[UNICEF Partner Reporting Portal] Due Report \ No newline at end of file diff --git a/django_api/django_api/templates/emails/overdue_progress_report.html b/django_api/django_api/templates/emails/overdue_progress_report.html new file mode 100644 index 000000000..defb18575 --- /dev/null +++ b/django_api/django_api/templates/emails/overdue_progress_report.html @@ -0,0 +1,16 @@ +{% extends 'emails/base.html' %} + +{% block content %} +

Dear {{ person.name }},

+

Please note that the following is overdue:

+
+

Report: {{progress_report.report_type}}{{progress_report.report_number}}

+

Period: {{progress_report.start_date}} - {{progress_report.end_date}}

+

Due Date: {{progress_report.due_date}}

+

Programme Document Ref: {{programme_document.reference_number}}

+

Programme Document Title: {{programme_document.title}}

+
+

Kind regards,

+

UNICEF

+

-- Please note that replies to this email are not monitored and cannot be replied to --

+{% endblock content %} \ No newline at end of file diff --git a/django_api/django_api/templates/emails/overdue_progress_report_subject.txt b/django_api/django_api/templates/emails/overdue_progress_report_subject.txt new file mode 100644 index 000000000..ff215a266 --- /dev/null +++ b/django_api/django_api/templates/emails/overdue_progress_report_subject.txt @@ -0,0 +1 @@ +[UNICEF Partner Reporting Portal] Overdue Report \ No newline at end of file From fcd5d474aabbd99d8535c0506e14b26f2a3662b7 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 4 Mar 2019 16:03:04 -0800 Subject: [PATCH 007/817] test: Wrote test for email notification post_save signal [#1075] + Added post_save mute decorator to ProgressReportFactory + Submitted status + Sent back status + Accepted status --- django_api/django_api/apps/core/factories.py | 1 + .../apps/unicef/tests/test_models.py | 300 ++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 django_api/django_api/apps/unicef/tests/test_models.py diff --git a/django_api/django_api/apps/core/factories.py b/django_api/django_api/apps/core/factories.py index 336072820..8253d26e3 100644 --- a/django_api/django_api/apps/core/factories.py +++ b/django_api/django_api/apps/core/factories.py @@ -1205,6 +1205,7 @@ class Meta: django_get_or_create = ('location', 'reportable') +@factory.django.mute_signals(signals.post_save) class ProgressReportFactory(factory.django.DjangoModelFactory): """ Arguments: diff --git a/django_api/django_api/apps/unicef/tests/test_models.py b/django_api/django_api/apps/unicef/tests/test_models.py new file mode 100644 index 000000000..7e87cc643 --- /dev/null +++ b/django_api/django_api/apps/unicef/tests/test_models.py @@ -0,0 +1,300 @@ +from django.core import mail +from django.conf import settings +from django.db.models import Q + +from core.common import ( + INDICATOR_REPORT_STATUS, + OVERALL_STATUS, + PROGRESS_REPORT_STATUS, + PRP_ROLE_TYPES, +) +from core.management.commands._generate_disaggregation_fake_data import ( + generate_3_num_disagg_data, +) +from core.factories import (CartoDBTableFactory, + ProgressReportIndicatorReportFactory, + IPPRPRoleFactory, + CountryFactory, DisaggregationFactory, + DisaggregationValueFactory, GatewayTypeFactory, + LocationFactory, + LocationWithReportableLocationGoalFactory, + PartnerUserFactory, PartnerFactory, + ProgressReportFactory, + QuantityReportableToLowerLevelOutputFactory, + QuantityTypeIndicatorBlueprintFactory, + WorkspaceFactory, + SectionFactory, + PersonFactory, + IPDisaggregationFactory, + ProgrammeDocumentFactory, + QPRReportingPeriodDatesFactory, + HRReportingPeriodDatesFactory, + PDResultLinkFactory, + LowerLevelOutputFactory, + ClusterPRPRoleFactory, + ResponsePlanFactory, + ClusterFactory, + NonPartnerUserFactory, + ClusterObjectiveFactory, + ClusterActivityFactory, + PartnerProjectFactory, + ClusterActivityPartnerActivityFactory, + QuantityReportableToPartnerActivityFactory, + ClusterIndicatorReportFactory) +from core.tests.base import BaseAPITestCase +from core.models import Location +from indicator.disaggregators import QuantityIndicatorDisaggregator +from indicator.models import ( + IndicatorBlueprint, + IndicatorLocationData, +) +from unicef.models import ProgressReport + + +class TestProgressReportModel(BaseAPITestCase): + + def setUp(self): + self.country = CountryFactory() + self.workspace = WorkspaceFactory(countries=[self.country, ]) + self.response_plan = ResponsePlanFactory(workspace=self.workspace) + self.cluster = ClusterFactory(type='cccm', response_plan=self.response_plan) + self.loc_type = GatewayTypeFactory(country=self.country) + self.carto_table = CartoDBTableFactory(location_type=self.loc_type, country=self.country) + self.loc1 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.loc2 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.unicef_officer = PersonFactory() + self.unicef_focal_point = PersonFactory() + self.partner_focal_point = PersonFactory() + self.objective = ClusterObjectiveFactory( + cluster=self.cluster, + locations=[ + self.loc1, + self.loc2, + ] + ) + self.activity = ClusterActivityFactory( + cluster_objective=self.objective, + locations=[ + self.loc1, self.loc2 + ] + ) + self.partner = PartnerFactory(country_code=self.country.country_short_code) + self.user = NonPartnerUserFactory() + self.partner_user = PartnerUserFactory(partner=self.partner) + ClusterPRPRoleFactory(user=self.user, workspace=self.workspace, cluster=self.cluster, role=PRP_ROLE_TYPES.cluster_imo) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, role=PRP_ROLE_TYPES.ip_authorized_officer) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, cluster=None, role=PRP_ROLE_TYPES.cluster_member) + self.project = PartnerProjectFactory( + partner=self.partner, + clusters=[self.cluster], + locations=[self.loc1, self.loc2], + ) + self.p_activity = ClusterActivityPartnerActivityFactory( + cluster_activity=self.activity, + project=self.project, + ) + self.sample_disaggregation_value_map = { + "height": ["tall", "medium", "short", "extrashort"], + "age": ["1-2m", "3-4m", "5-6m", '7-10m', '11-13m', '14-16m'], + "gender": ["male", "female", "other"], + } + + blueprint = QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + calculation_formula_across_periods=IndicatorBlueprint.SUM, + ) + self.partneractivity_reportable = QuantityReportableToPartnerActivityFactory( + content_object=self.p_activity, blueprint=blueprint + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.partneractivity_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.partneractivity_reportable, + ) + + self.pd = ProgrammeDocumentFactory( + workspace=self.workspace, + partner=self.partner, + sections=[SectionFactory(), ], + unicef_officers=[self.unicef_officer, ], + unicef_focal_point=[self.unicef_focal_point, ], + partner_focal_point=[self.partner_focal_point, ] + ) + + for idx in range(2): + qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) + ProgressReportFactory( + start_date=qpr_period.start_date, + end_date=qpr_period.end_date, + due_date=qpr_period.due_date, + report_number=idx + 1, + report_type=qpr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + for idx in range(6): + hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) + ProgressReportFactory( + start_date=hr_period.start_date, + end_date=hr_period.end_date, + due_date=hr_period.due_date, + report_number=idx + 1, + report_type=hr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + self.cp_output = PDResultLinkFactory( + programme_document=self.pd, + ) + self.llo = LowerLevelOutputFactory( + cp_output=self.cp_output, + ) + self.llo_reportable = QuantityReportableToLowerLevelOutputFactory( + content_object=self.llo, + blueprint=QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + ) + ) + + self.llo_reportable.disaggregations.clear() + self.partneractivity_reportable.disaggregations.clear() + + # Create the disaggregations and values in the db for all response plans + # including one for no response plan as well + for disagg_name, values in self.sample_disaggregation_value_map.items(): + disagg = IPDisaggregationFactory(name=disagg_name) + cluster_disagg = DisaggregationFactory(name=disagg_name, response_plan=self.response_plan) + + self.llo_reportable.disaggregations.add(disagg) + self.partneractivity_reportable.disaggregations.add(cluster_disagg) + + for value in values: + DisaggregationValueFactory( + disaggregation=cluster_disagg, + value=value + ) + DisaggregationValueFactory( + disaggregation=disagg, + value=value + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.llo_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.llo_reportable, + ) + + for _ in range(2): + ClusterIndicatorReportFactory( + reportable=self.partneractivity_reportable, + report_status=INDICATOR_REPORT_STATUS.submitted, + ) + + # Creating Level-3 disaggregation location data for all locations + generate_3_num_disagg_data(self.partneractivity_reportable, indicator_type="quantity") + + for loc_data in IndicatorLocationData.objects.filter(indicator_report__reportable=self.partneractivity_reportable): + QuantityIndicatorDisaggregator.post_process(loc_data) + + for pr in self.pd.progress_reports.all(): + ProgressReportIndicatorReportFactory( + progress_report=pr, + reportable=self.llo_reportable, + report_status=INDICATOR_REPORT_STATUS.submitted, + overall_status=OVERALL_STATUS.met, + ) + + # Creating Level-3 disaggregation location data for all locations + generate_3_num_disagg_data(self.llo_reportable, indicator_type="quantity") + + for loc_data in IndicatorLocationData.objects.filter(indicator_report__reportable=self.llo_reportable): + QuantityIndicatorDisaggregator.post_process(loc_data) + + super().setUp() + + # Logging in as Partner AO + self.client.force_authenticate(self.partner_user) + + self.location_id = self.loc1.id + self.queryset = self.get_queryset() + + def get_queryset(self): + pd_ids = Location.objects.filter( + Q(id=self.location_id) | + Q(parent_id=self.location_id) | + Q(parent__parent_id=self.location_id) | + Q(parent__parent__parent_id=self.location_id) | + Q(parent__parent__parent__parent_id=self.location_id) + ).values_list( + 'reportables__lower_level_outputs__cp_output__programme_document__id', + flat=True + ) + return ProgressReport.objects.filter(programme_document_id__in=pd_ids) + + def test_send_notification_on_status_change_post_save_signal_for_submitted(self): + mail.outbox = [] + report = ProgressReport.objects.get(report_type="QPR", report_number=1) + + report.status = PROGRESS_REPORT_STATUS.submitted + report.save() + + # Match # of emails sent by submitting_user (also identical to submitted_by), and unicef_focal_point + self.assertEqual(len(mail.outbox), 2) + + sent_mail = mail.outbox[-1] + + self.assertIn("Please note that the following was submitted to UNICEF:", sent_mail.body) + report.status = PROGRESS_REPORT_STATUS.due + report.save() + + def test_send_notification_on_status_change_post_save_signal_for_sent_back(self): + mail.outbox = [] + report = ProgressReport.objects.get(report_type="QPR", report_number=1) + + report.status = PROGRESS_REPORT_STATUS.sent_back + report.save() + + # Match # of emails sent by submitting_user (also identical to submitted_by), and unicef_focal_point + self.assertEqual(len(mail.outbox), 2) + + sent_mail = mail.outbox[-1] + + self.assertIn("Please note that the following was sent back by UNICEF:", sent_mail.body) + self.assertIn( + "Please log in to the UNICEF Partner Reporting Portal to see the reason " + + "and contact details of the person that sent the report back.", sent_mail.body) + report.status = PROGRESS_REPORT_STATUS.due + report.save() + + def test_send_notification_on_status_change_post_save_signal_for_accepted(self): + mail.outbox = [] + report = ProgressReport.objects.get(report_type="QPR", report_number=1) + + report.status = PROGRESS_REPORT_STATUS.accepted + report.save() + + # Match # of emails sent by submitting_user (also identical to submitted_by), and unicef_focal_point + self.assertEqual(len(mail.outbox), 2) + + sent_mail = mail.outbox[-1] + + self.assertIn("Please note that the following was accepted by UNICEF:", sent_mail.body) + report.status = PROGRESS_REPORT_STATUS.due + report.save() From d89e9f0a3ccc9849806574730ba204054273510e Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 4 Mar 2019 16:12:14 -0800 Subject: [PATCH 008/817] refactor: Refactored task logics to call a single function for test coverage purposes [#1074] --- django_api/django_api/apps/indicator/tasks.py | 33 +---- django_api/django_api/apps/utils/emails.py | 120 +++++++++++------- 2 files changed, 79 insertions(+), 74 deletions(-) diff --git a/django_api/django_api/apps/indicator/tasks.py b/django_api/django_api/apps/indicator/tasks.py index c693a4aef..c888f7d57 100644 --- a/django_api/django_api/apps/indicator/tasks.py +++ b/django_api/django_api/apps/indicator/tasks.py @@ -1,6 +1,5 @@ import logging -from datetime import datetime, date, timedelta -from dateutil.relativedelta import relativedelta +from datetime import datetime, timedelta from celery import shared_task @@ -56,35 +55,9 @@ def process_due_reports(): @shared_task def notify_ip_due_reports(): - logger.info("Notifying IP due progress reports") - notified = list() - - today = date.today() - unsubmitted_due_reports = ProgressReport.objects.filter( - submission_date__isnull=True, - status=PROGRESS_REPORT_STATUS.due, - due_date=today + relativedelta(days=7), - ) - - for report in unsubmitted_due_reports: - send_due_progress_report_email(report) - notified.append(report.id) - - return "Sent emails for %s Due Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" + return send_due_progress_report_email() @shared_task def notify_ip_overdue_reports(): - logger.info("Notifying IP overdue progress reports") - notified = list() - - unsubmitted_overdue_reports = ProgressReport.objects.filter( - submission_date__isnull=True, - status=PROGRESS_REPORT_STATUS.overdue, - ) - - for report in unsubmitted_overdue_reports: - send_overdue_progress_report_email(report) - notified.append(report.id) - - return "Sent emails for %s Overdue Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" + return send_overdue_progress_report_email() diff --git a/django_api/django_api/apps/utils/emails.py b/django_api/django_api/apps/utils/emails.py index c14cda79c..b23152675 100644 --- a/django_api/django_api/apps/utils/emails.py +++ b/django_api/django_api/apps/utils/emails.py @@ -1,7 +1,17 @@ +import logging +from datetime import date +from dateutil.relativedelta import relativedelta + from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string +from core.common import PROGRESS_REPORT_STATUS +from unicef.models import ProgressReport + + +logger = logging.getLogger(__name__) + def send_email_from_template( subject_template_path, @@ -39,61 +49,83 @@ def send_email_from_template( message.send(fail_silently=fail_silently) -def send_due_progress_report_email(progress_report): +def send_due_progress_report_email(): """send_due_progress_report_email sends email notifications to UNICEF Authorized Officers and Focal Points about reports due in 1 week. - - Arguments: - progress_report {ProgressReport} -- ProgressReport ORM object to send email notification about """ - pd = progress_report.programme_document + logger.info("Notifying IP due progress reports") + notified = list() + + today = date.today() + unsubmitted_due_reports = ProgressReport.objects.filter( + submission_date__isnull=True, + status=PROGRESS_REPORT_STATUS.due, + due_date=today + relativedelta(days=7), + ) + + for progress_report in unsubmitted_due_reports: + pd = progress_report.programme_document + + template_data = { + 'person': None, + 'progress_report': progress_report, + 'programme_document': pd, + } + + to_emails = set() + to_emails |= set(map(lambda person: person, pd.unicef_officers.all())) + to_emails |= set(map(lambda person: person, pd.unicef_focal_point.all())) - template_data = { - 'person': None, - 'progress_report': progress_report, - 'programme_document': pd, - } + for person in to_emails: + send_email_from_template( + subject_template_path='emails/due_progress_report_subject.txt', + body_template_path='emails/due_progress_report.html', + template_data=template_data, + to_email_list=[person.email, ], + content_subtype='html' + ) - to_emails = set() - to_emails |= set(map(lambda person: person, pd.unicef_officers.all())) - to_emails |= set(map(lambda person: person, pd.unicef_focal_point.all())) + notified.append(progress_report.id) - for person in to_emails: - send_email_from_template( - subject_template_path='emails/due_progress_report_subject.txt', - body_template_path='emails/due_progress_report.html', - template_data=template_data, - to_email_list=[person.email, ], - content_subtype='html' - ) + return "Sent emails for %s Due Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" def send_overdue_progress_report_email(progress_report): """send_overdue_progress_report_email sends email notifications to UNICEF Authorized Officers and Focal Points about overdue reports. - - Arguments: - progress_report {ProgressReport} -- ProgressReport ORM object to send email notification about """ - pd = progress_report.programme_document - - template_data = { - 'person': None, - 'progress_report': progress_report, - 'programme_document': pd, - } - - to_emails = set() - to_emails |= set(map(lambda person: person, pd.unicef_officers.all())) - to_emails |= set(map(lambda person: person, pd.unicef_focal_point.all())) - - for person in to_emails: - send_email_from_template( - subject_template_path='emails/overdue_progress_report_subject.txt', - body_template_path='emails/overdue_progress_report.html', - template_data=template_data, - to_email_list=[person.email, ], - content_subtype='html' - ) + logger.info("Notifying IP overdue progress reports") + notified = list() + + unsubmitted_overdue_reports = ProgressReport.objects.filter( + submission_date__isnull=True, + status=PROGRESS_REPORT_STATUS.overdue, + ) + + for report in unsubmitted_overdue_reports: + pd = progress_report.programme_document + + template_data = { + 'person': None, + 'progress_report': progress_report, + 'programme_document': pd, + } + + to_emails = set() + to_emails |= set(map(lambda person: person, pd.unicef_officers.all())) + to_emails |= set(map(lambda person: person, pd.unicef_focal_point.all())) + + for person in to_emails: + send_email_from_template( + subject_template_path='emails/overdue_progress_report_subject.txt', + body_template_path='emails/overdue_progress_report.html', + template_data=template_data, + to_email_list=[person.email, ], + content_subtype='html' + ) + + notified.append(report.id) + + return "Sent emails for %s Overdue Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" From da3b927097f3fc45a6f4b3cfccd4061d203f3155 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 4 Mar 2019 16:17:15 -0800 Subject: [PATCH 009/817] fix: Resolved circular dependencies [#1074] --- django_api/django_api/apps/utils/emails.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/django_api/django_api/apps/utils/emails.py b/django_api/django_api/apps/utils/emails.py index b23152675..f38d70764 100644 --- a/django_api/django_api/apps/utils/emails.py +++ b/django_api/django_api/apps/utils/emails.py @@ -1,14 +1,9 @@ import logging -from datetime import date -from dateutil.relativedelta import relativedelta from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string -from core.common import PROGRESS_REPORT_STATUS -from unicef.models import ProgressReport - logger = logging.getLogger(__name__) @@ -53,6 +48,10 @@ def send_due_progress_report_email(): """send_due_progress_report_email sends email notifications to UNICEF Authorized Officers and Focal Points about reports due in 1 week. """ + from datetime import date + from dateutil.relativedelta import relativedelta + from core.common import PROGRESS_REPORT_STATUS + from unicef.models import ProgressReport logger.info("Notifying IP due progress reports") notified = list() @@ -95,6 +94,8 @@ def send_overdue_progress_report_email(progress_report): """send_overdue_progress_report_email sends email notifications to UNICEF Authorized Officers and Focal Points about overdue reports. """ + from core.common import PROGRESS_REPORT_STATUS + from unicef.models import ProgressReport logger.info("Notifying IP overdue progress reports") notified = list() From 0aa936129ba6695396d63674ee9bf7247db182b4 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 4 Mar 2019 16:36:53 -0800 Subject: [PATCH 010/817] test: Wrote test cases for due & overdue progress report email notifications [#1074] + test_send_due_progress_report_email + test_send_overdue_progress_report_email + Refactoring on return value for email notification functions --- .../apps/unicef/tests/test_models.py | 50 +++++++++++++++++-- django_api/django_api/apps/utils/emails.py | 12 ++--- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/django_api/django_api/apps/unicef/tests/test_models.py b/django_api/django_api/apps/unicef/tests/test_models.py index 7e87cc643..37bdeb3a8 100644 --- a/django_api/django_api/apps/unicef/tests/test_models.py +++ b/django_api/django_api/apps/unicef/tests/test_models.py @@ -1,3 +1,6 @@ +from datetime import date +from dateutil.relativedelta import relativedelta + from django.core import mail from django.conf import settings from django.db.models import Q @@ -43,6 +46,7 @@ ClusterIndicatorReportFactory) from core.tests.base import BaseAPITestCase from core.models import Location +from utils.emails import send_due_progress_report_email, send_overdue_progress_report_email from indicator.disaggregators import QuantityIndicatorDisaggregator from indicator.models import ( IndicatorBlueprint, @@ -251,6 +255,8 @@ def get_queryset(self): def test_send_notification_on_status_change_post_save_signal_for_submitted(self): mail.outbox = [] report = ProgressReport.objects.get(report_type="QPR", report_number=1) + report.status = PROGRESS_REPORT_STATUS.due + report.save() report.status = PROGRESS_REPORT_STATUS.submitted report.save() @@ -261,12 +267,12 @@ def test_send_notification_on_status_change_post_save_signal_for_submitted(self) sent_mail = mail.outbox[-1] self.assertIn("Please note that the following was submitted to UNICEF:", sent_mail.body) - report.status = PROGRESS_REPORT_STATUS.due - report.save() def test_send_notification_on_status_change_post_save_signal_for_sent_back(self): mail.outbox = [] report = ProgressReport.objects.get(report_type="QPR", report_number=1) + report.status = PROGRESS_REPORT_STATUS.due + report.save() report.status = PROGRESS_REPORT_STATUS.sent_back report.save() @@ -280,12 +286,12 @@ def test_send_notification_on_status_change_post_save_signal_for_sent_back(self) self.assertIn( "Please log in to the UNICEF Partner Reporting Portal to see the reason " + "and contact details of the person that sent the report back.", sent_mail.body) - report.status = PROGRESS_REPORT_STATUS.due - report.save() def test_send_notification_on_status_change_post_save_signal_for_accepted(self): mail.outbox = [] report = ProgressReport.objects.get(report_type="QPR", report_number=1) + report.status = PROGRESS_REPORT_STATUS.due + report.save() report.status = PROGRESS_REPORT_STATUS.accepted report.save() @@ -296,5 +302,41 @@ def test_send_notification_on_status_change_post_save_signal_for_accepted(self): sent_mail = mail.outbox[-1] self.assertIn("Please note that the following was accepted by UNICEF:", sent_mail.body) + + def test_send_due_progress_report_email(self): + mail.outbox = [] + today = date.today() + + report = ProgressReport.objects.get(report_type="QPR", report_number=1) report.status = PROGRESS_REPORT_STATUS.due + report.due_date = today + relativedelta(days=7) + report.submission_date = None + report.save() + + send_due_progress_report_email() + + # Match # of emails sent by unicef_officer and unicef_focal_point + self.assertEqual(len(mail.outbox), 2) + + sent_mail = mail.outbox[-1] + + self.assertIn("Please note that the following is due in 1 week:", sent_mail.body) + + def test_send_overdue_progress_report_email(self): + mail.outbox = [] + today = date.today() + + report = ProgressReport.objects.get(report_type="QPR", report_number=1) + report.status = PROGRESS_REPORT_STATUS.overdue + report.due_date = today - relativedelta(days=30) + report.submission_date = None report.save() + + send_overdue_progress_report_email() + + # Match # of emails sent by unicef_officer and unicef_focal_point + self.assertEqual(len(mail.outbox), 2) + + sent_mail = mail.outbox[-1] + + self.assertIn("Please note that the following is overdue:", sent_mail.body) diff --git a/django_api/django_api/apps/utils/emails.py b/django_api/django_api/apps/utils/emails.py index f38d70764..8ec59f901 100644 --- a/django_api/django_api/apps/utils/emails.py +++ b/django_api/django_api/apps/utils/emails.py @@ -85,12 +85,12 @@ def send_due_progress_report_email(): content_subtype='html' ) - notified.append(progress_report.id) + notified.append(str(progress_report.id)) - return "Sent emails for %s Due Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" + return "Sent emails for {} Due Report IDs: {}".format(len(notified), ", ".join(notified)) if notified else "---" -def send_overdue_progress_report_email(progress_report): +def send_overdue_progress_report_email(): """send_overdue_progress_report_email sends email notifications to UNICEF Authorized Officers and Focal Points about overdue reports. """ @@ -105,7 +105,7 @@ def send_overdue_progress_report_email(progress_report): status=PROGRESS_REPORT_STATUS.overdue, ) - for report in unsubmitted_overdue_reports: + for progress_report in unsubmitted_overdue_reports: pd = progress_report.programme_document template_data = { @@ -127,6 +127,6 @@ def send_overdue_progress_report_email(progress_report): content_subtype='html' ) - notified.append(report.id) + notified.append(str(progress_report.id)) - return "Sent emails for %s Overdue Report IDs: %s" % (len(notified), ", ".join(notified)) if notified else "---" + return "Sent emails for {} Overdue Report IDs: {}".format(len(notified), ", ".join(notified)) if notified else "---" From 7b4adee4410f705e48f7973ef4aa2f393ffa13cf Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 4 Mar 2019 16:39:56 -0800 Subject: [PATCH 011/817] lint: Fixed flake8 errors [#1074] --- django_api/django_api/apps/unicef/tests/test_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/django_api/django_api/apps/unicef/tests/test_models.py b/django_api/django_api/apps/unicef/tests/test_models.py index 37bdeb3a8..79ef1e137 100644 --- a/django_api/django_api/apps/unicef/tests/test_models.py +++ b/django_api/django_api/apps/unicef/tests/test_models.py @@ -2,7 +2,6 @@ from dateutil.relativedelta import relativedelta from django.core import mail -from django.conf import settings from django.db.models import Q from core.common import ( From 8293f35deaa93081f4b71f8a6d8d4f460e296fa5 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Tue, 5 Mar 2019 16:29:46 -0800 Subject: [PATCH 012/817] feat: Created ProgressReportAttachment model and removed attachment field in ProgressReport [#1088] + DB migrations + Data migration file + Forward/Backward migration logic --- .../0007_progressreportattachment.py | 32 ++++++++++ .../migrations/0008_auto_20190305_2323.py | 58 +++++++++++++++++++ .../0009_remove_progressreport_attachment.py | 19 ++++++ django_api/django_api/apps/unicef/models.py | 28 +++++++-- 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py create mode 100644 django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py create mode 100644 django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py diff --git a/django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py b/django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py new file mode 100644 index 000000000..b23f11168 --- /dev/null +++ b/django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-05 23:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import unicef.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('unicef', '0006_auto_20181204_0013'), + ] + + operations = [ + migrations.CreateModel( + name='ProgressReportAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('file', models.FileField(upload_to=unicef.models.get_pr_attachment_upload_to)), + ('progress_report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='unicef.ProgressReport')), + ], + options={ + 'ordering': ['id'], + }, + ), + ] diff --git a/django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py b/django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py new file mode 100644 index 000000000..65230d630 --- /dev/null +++ b/django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-05 23:23 +from __future__ import unicode_literals + +from django.core.files.base import ContentFile +from django.db import migrations + + +def forwards_func(apps, schema_editor): + ProgressReport = apps.get_model("unicef", "ProgressReport") + ProgressReportAttachment = apps.get_model("unicef", "ProgressReportAttachment") + + for item in ProgressReport.objects.all(): + try: + new_file = ContentFile(item.attachment.read()) + new_file.name = item.attachment.name.split('/')[-1] + new_file.path = f"unicef/progress_reports/{item.id}/{new_file.name}" + + ProgressReportAttachment.objects.create( + progress_report=item, + file=new_file, + ) + + item.attachment.delete() + + except ValueError: + pass + + +def reverse_func(apps, schema_editor): + ProgressReportAttachment = apps.get_model("unicef", "ProgressReportAttachment") + + for item in ProgressReportAttachment.objects.all(): + try: + # Only do overwrite if ProgressReport's attachment field is None + new_file = ContentFile(item.file.read()) + new_file.name = item.file.name.split('/')[-1] + new_file.path = f"unicef/progress_reports/{new_file.name}" + + item.progress_report.attachment = new_file + item.progress_report.save() + + item.file.delete() + except ValueError: + pass + + ProgressReportAttachment.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('unicef', '0007_progressreportattachment'), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + ] diff --git a/django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py b/django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py new file mode 100644 index 000000000..746866b98 --- /dev/null +++ b/django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-06 00:28 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('unicef', '0008_auto_20190305_2323'), + ] + + operations = [ + migrations.RemoveField( + model_name='progressreport', + name='attachment', + ), + ] diff --git a/django_api/django_api/apps/unicef/models.py b/django_api/django_api/apps/unicef/models.py index 8356f894b..479e3a79f 100644 --- a/django_api/django_api/apps/unicef/models.py +++ b/django_api/django_api/apps/unicef/models.py @@ -406,11 +406,6 @@ class ProgressReport(TimeStampedModel): null=True ) sent_back_feedback = models.TextField(blank=True, null=True) - attachment = models.FileField( - upload_to="unicef/progress_reports/", - blank=True, - null=True - ) report_number = models.IntegerField(verbose_name="Report Number") report_type = models.CharField(verbose_name="Report type", choices=REPORTING_TYPES, max_length=3) is_final = models.BooleanField(verbose_name="Is final report", default=False) @@ -498,6 +493,29 @@ def send_notification_on_status_change(sender, instance, **kwargs): ) +def get_pr_attachment_upload_to(instance, filename): + return f"unicef/progress_reports/{instance.progress_report.id}/{filename}" + + +class ProgressReportAttachment(TimeStampedModel): + """ + ProgressReportAttachment represents an attachment file for ProgressReport. + + related models: + unicef.ProgressReport (ForeignKey): "progress_report" + """ + progress_report = models.ForeignKey('unicef.ProgressReport', related_name="attachments") + file = models.FileField( + upload_to=get_pr_attachment_upload_to, + ) + + class Meta: + ordering = ['id'] + + def __str__(self): + return self.file.name + + class ReportingPeriodDates(TimeStampedExternalBusinessAreaModel): """ Used for storing start_date, end_date and due_date fields for multiple reports From 2c6263069f69d273b6032111844d6a52949af5e1 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 6 Mar 2019 12:07:18 -0800 Subject: [PATCH 013/817] feat: Added a new type field for ProgressReportAttachment [#1088] + DB migration --- django_api/django_api/apps/core/common.py | 5 +++++ .../0010_progressreportattachment_type.py | 21 +++++++++++++++++++ django_api/django_api/apps/unicef/models.py | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py diff --git a/django_api/django_api/apps/core/common.py b/django_api/django_api/apps/core/common.py index 2a478b9a8..a94c71ddb 100644 --- a/django_api/django_api/apps/core/common.py +++ b/django_api/django_api/apps/core/common.py @@ -174,6 +174,11 @@ ("OTHER", "other", "Other"), # Flash Appeal ) +PR_ATTACHMENT_TYPES = Choices( + ("FACE", "face", "FACE"), + ("Other", "other", "Other"), +) + OVERALL_STATUS = Choices( ("Met", "met", "Met"), ("OnT", "on_track", "On Track"), diff --git a/django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py b/django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py new file mode 100644 index 000000000..ffd8efd29 --- /dev/null +++ b/django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-06 20:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('unicef', '0009_remove_progressreport_attachment'), + ] + + operations = [ + migrations.AddField( + model_name='progressreportattachment', + name='type', + field=models.CharField(choices=[('FACE', 'FACE'), ('Other', 'Other')], default='Other', max_length=5, verbose_name='Attachment type'), + preserve_default=False, + ), + ] diff --git a/django_api/django_api/apps/unicef/models.py b/django_api/django_api/apps/unicef/models.py index 479e3a79f..bf38a2eac 100644 --- a/django_api/django_api/apps/unicef/models.py +++ b/django_api/django_api/apps/unicef/models.py @@ -25,6 +25,7 @@ CURRENCIES, OVERALL_STATUS, REPORTING_TYPES, + PR_ATTACHMENT_TYPES, PRP_ROLE_TYPES, ) from core.models import TimeStampedExternalBusinessAreaModel, TimeStampedExternalSyncModelMixin @@ -508,6 +509,7 @@ class ProgressReportAttachment(TimeStampedModel): file = models.FileField( upload_to=get_pr_attachment_upload_to, ) + type = models.CharField(verbose_name="Attachment type", choices=PR_ATTACHMENT_TYPES, max_length=5) class Meta: ordering = ['id'] From 2cc481cb53b0382ec4e874edfc1b18cda6131a21 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 6 Mar 2019 16:19:17 -0800 Subject: [PATCH 014/817] feat: Updated ProgressReportAttachmentSerializer [#1088] + Use ProgressReportAttachment model + Change the FileField reference --- .../django_api/apps/unicef/serializers.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/django_api/django_api/apps/unicef/serializers.py b/django_api/django_api/apps/unicef/serializers.py index 5cc05edd0..0c73c3f52 100644 --- a/django_api/django_api/apps/unicef/serializers.py +++ b/django_api/django_api/apps/unicef/serializers.py @@ -1,10 +1,16 @@ from django.conf import settings +from django.db import transaction + from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied, ValidationError from core.serializers import ShortLocationSerializer from utils.filters.constants import Boolean -from .models import ProgrammeDocument, Section, ProgressReport, Person, \ - LowerLevelOutput, PDResultLink, ReportingPeriodDates +from .models import ( + ProgrammeDocument, Section, ProgressReport, Person, + LowerLevelOutput, PDResultLink, ReportingPeriodDates, + ProgressReportAttachment, +) from core.common import PROGRESS_REPORT_STATUS, OVERALL_STATUS, CURRENCIES, PD_STATUS from core.models import Workspace, Location @@ -784,26 +790,28 @@ class Meta: class ProgressReportAttachmentSerializer(serializers.ModelSerializer): size = serializers.SerializerMethodField() file_name = serializers.SerializerMethodField() - path = serializers.FileField(source='attachment') + path = serializers.FileField(source='file') def get_file_name(self, obj): - return obj.attachment.name.split('/')[-1] if obj.attachment else None + return obj.file.name.split('/')[-1] if obj.file else None def get_size(self, obj): - return obj.attachment.size if obj.attachment else None + return obj.file.size if obj.file else None def to_representation(self, instance): representation = super(ProgressReportAttachmentSerializer, self).to_representation(instance) - if instance.attachment and "http" not in instance.attachment.url: - representation['path'] = settings.WWW_ROOT[:-1] + instance.attachment.url + if instance.file and "http" not in instance.file.url: + representation['path'] = settings.WWW_ROOT[:-1] + instance.file.url return representation class Meta: - model = ProgressReport + model = ProgressReportAttachment fields = ( + 'id', 'path', 'size', - 'file_name' + 'file_name', + 'type', ) From 1a9c77d5d8355c48f890b7dab23bab88a71a29c8 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 6 Mar 2019 16:21:15 -0800 Subject: [PATCH 015/817] feat: Created ProgressReportAttachmentListCreateAPIView class [#1088] + ListCreate API for ProgressReportAttachment + perform_create method for validating # of attachments per type + Refactored ProgressReportAttachmentAPIView to use ProgressReportAttachment model --- django_api/django_api/apps/indicator/views.py | 6 +- django_api/django_api/apps/unicef/urls.py | 7 +- django_api/django_api/apps/unicef/views.py | 86 +++++++++++++------ 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/django_api/django_api/apps/indicator/views.py b/django_api/django_api/apps/indicator/views.py index d8984ac64..1cfb1bb1f 100644 --- a/django_api/django_api/apps/indicator/views.py +++ b/django_api/django_api/apps/indicator/views.py @@ -131,7 +131,11 @@ class PDReportsDetailAPIView(RetrieveAPIView): def check_permissions(self, request): super().check_permissions(request) - pd_id = self.kwargs['pd_id'] + pd_id = self.kwargs.get('pd_id', None) + + if not pd_id: + self.permission_denied(request) + if not request.user.partner.programmedocument_set.filter(id=pd_id).exists(): self.permission_denied(request) diff --git a/django_api/django_api/apps/unicef/urls.py b/django_api/django_api/apps/unicef/urls.py index 79f9a5e36..fb60ff86b 100644 --- a/django_api/django_api/apps/unicef/urls.py +++ b/django_api/django_api/apps/unicef/urls.py @@ -15,6 +15,7 @@ ProgrammeDocumentCalculationMethodsAPIView, ProgrammeDocumentProgressAPIView, ProgressReportDetailsUpdateAPIView, + ProgressReportAttachmentListCreateAPIView, ProgressReportAttachmentAPIView, ProgressReportSRSubmitAPIView, ProgressReportPullHFDataAPIView, @@ -82,8 +83,10 @@ url(r'^(?P\d+)/progress-reports/(?P\d+)/locations/$', ProgressReportLocationsAPIView.as_view(), name="progress-reports-locations"), - url(r'^(?P\d+)/progress-reports/(?P\d+)/attachment/$', + url(r'^(?P\d+)/progress-reports/(?P\d+)/attachments/$', + ProgressReportAttachmentListCreateAPIView.as_view(), + name="progress-reports-attachment-list"), + url(r'^(?P\d+)/progress-reports/(?P\d+)/attachments/(?P\d+)/$', ProgressReportAttachmentAPIView.as_view(), name="progress-reports-attachment"), - ] diff --git a/django_api/django_api/apps/unicef/views.py b/django_api/django_api/apps/unicef/views.py index dc9cfe32e..1eb8de742 100644 --- a/django_api/django_api/apps/unicef/views.py +++ b/django_api/django_api/apps/unicef/views.py @@ -10,7 +10,7 @@ from rest_framework import status as statuses from rest_framework.exceptions import ValidationError -from rest_framework.generics import RetrieveAPIView, ListAPIView +from rest_framework.generics import RetrieveAPIView, ListAPIView, ListCreateAPIView from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser from rest_framework.response import Response from rest_framework.views import APIView @@ -25,6 +25,7 @@ INDICATOR_REPORT_STATUS, OVERALL_STATUS, PD_STATUS, + PR_ATTACHMENT_TYPES, PRP_ROLE_TYPES, ) from core.paginations import SmallPagination @@ -72,7 +73,7 @@ ProgressReportSRUpdateSerializer, ProgressReportPullHFDataSerializer, ) -from .models import ProgrammeDocument, ProgressReport, LowerLevelOutput +from .models import ProgrammeDocument, ProgressReport, LowerLevelOutput, ProgressReportAttachment from .permissions import ( CanChangePDCalculationMethod, UnicefPartnershipManagerOrRead @@ -1092,6 +1093,38 @@ def post(self, request, *args, **kwargs): return Response(serializer.data, status=statuses.HTTP_200_OK) +class ProgressReportAttachmentListCreateAPIView(ListCreateAPIView): + serializer_class = ProgressReportAttachmentSerializer + permission_classes = ( + AnyPermission( + IsUNICEFAPIUser, + IsPartnerAuthorizedOfficerForCurrentWorkspace, + IsPartnerEditorForCurrentWorkspace, + ), + ) + parser_classes = (FormParser, MultiPartParser, FileUploadParser) + + def get_queryset(self): + return ProgressReportAttachment.objects.filter( + progress_report_id=self.kwargs['progress_report_id'], + progress_report__programme_document__workspace_id=self.kwargs['workspace_id'], + ) + + def perform_create(self, serializer): + if self.get_queryset().count() == 3: + raise ValidationError('This progress report already has 3 attachments') + + if serializer.validated_data['type'] == PR_ATTACHMENT_TYPES.face \ + and self.get_queryset().filter(type=PR_ATTACHMENT_TYPES.face).count() == 1: + raise ValidationError('This progress report already has 1 FACE attachment') + + if serializer.validated_data['type'] == PR_ATTACHMENT_TYPES.other \ + and self.get_queryset().filter(type=PR_ATTACHMENT_TYPES.other).count() == 2: + raise ValidationError('This progress report already has 2 Other attachments') + + serializer.save(progress_report_id=self.kwargs['progress_report_id']) + + class ProgressReportAttachmentAPIView(APIView): permission_classes = ( AnyPermission( @@ -1103,17 +1136,18 @@ class ProgressReportAttachmentAPIView(APIView): parser_classes = (FormParser, MultiPartParser, FileUploadParser) - def get(self, request, workspace_id, progress_report_id): - progress_report = get_object_or_404( - ProgressReport, - id=progress_report_id, - programme_document__workspace_id=workspace_id + def get(self, request, workspace_id, progress_report_id, pk): + attachment = get_object_or_404( + ProgressReportAttachment, + id=pk, + progress_report_id=progress_report_id, + progress_report__programme_document__workspace_id=workspace_id ) try: # lookup just so the possible FileNotFoundError can be triggered - progress_report.attachment - serializer = ProgressReportAttachmentSerializer(progress_report) + attachment.file + serializer = ProgressReportAttachmentSerializer(attachment) return Response(serializer.data, status=statuses.HTTP_200_OK) except FileNotFoundError: pass @@ -1121,16 +1155,18 @@ def get(self, request, workspace_id, progress_report_id): return Response({"message": "Attachment does not exist."}, status=statuses.HTTP_404_NOT_FOUND) @transaction.atomic - def delete(self, request, workspace_id, progress_report_id): - pr = get_object_or_404( - ProgressReport, - id=progress_report_id, - programme_document__workspace_id=workspace_id + def delete(self, request, workspace_id, progress_report_id, pk): + attachment = get_object_or_404( + ProgressReportAttachment, + id=pk, + progress_report_id=progress_report_id, + progress_report__programme_document__workspace_id=workspace_id ) - if pr.attachment: + if attachment.file: try: - pr.attachment.delete() + attachment.file.delete() + attachment.delete() return Response({}, status=statuses.HTTP_204_NO_CONTENT) except ValueError: pass @@ -1138,21 +1174,23 @@ def delete(self, request, workspace_id, progress_report_id): return Response({"message": "Attachment does not exist."}, status=statuses.HTTP_404_NOT_FOUND) @transaction.atomic - def put(self, request, workspace_id, progress_report_id): - pr = get_object_or_404( - ProgressReport, - id=progress_report_id, - programme_document__workspace_id=workspace_id) + def put(self, request, workspace_id, progress_report_id, pk): + attachment = get_object_or_404( + ProgressReportAttachment, + id=pk, + progress_report_id=progress_report_id, + progress_report__programme_document__workspace_id=workspace_id + ) serializer = ProgressReportAttachmentSerializer( - instance=pr, + instance=attachment, data=request.data ) serializer.is_valid(raise_exception=True) - if pr.attachment: + if attachment.file: try: - pr.attachment.delete() + attachment.file.delete() except ValueError: pass From f4b8b360ee7289cb19e1b9579d38f67dd7298645 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 12:13:51 -0800 Subject: [PATCH 016/817] feat: Created ProgressReportAttachmentFactory [#1088] + Refactored factories and fixed existing unicef test cases --- django_api/django_api/apps/core/factories.py | 104 +++++++-------- .../apps/unicef/tests/test_views.py | 122 +++++++++++------- 2 files changed, 124 insertions(+), 102 deletions(-) diff --git a/django_api/django_api/apps/core/factories.py b/django_api/django_api/apps/core/factories.py index 336072820..50ce7ffa6 100644 --- a/django_api/django_api/apps/core/factories.py +++ b/django_api/django_api/apps/core/factories.py @@ -2,74 +2,43 @@ import json import random from collections import defaultdict + +import factory from dateutil.relativedelta import relativedelta from django.contrib.contenttypes.models import ContentType +from django.core.files.base import ContentFile from django.db.models import signals -import factory from factory import fuzzy from faker import Faker from account.models import User, UserProfile -from cluster.models import Cluster, ClusterObjective, ClusterActivity -from partner.models import ( - Partner, - PartnerProject, - PartnerActivity, - PartnerProjectFunding, -) -from indicator.models import ( - IndicatorBlueprint, - Reportable, - IndicatorReport, - IndicatorLocationData, - Disaggregation, - DisaggregationValue, - ReportableLocationGoal, - ReportingEntity, -) -from unicef.models import ( - Section, - Person, - ProgressReport, - ProgrammeDocument, - PDResultLink, - LowerLevelOutput, - ReportingPeriodDates, -) -from core.common import ( - REPORTING_TYPES, - PRP_ROLE_TYPES, - CLUSTER_TYPES, - CSO_TYPES, - PARTNER_TYPE, - SHARED_PARTNER_TYPE, - INDICATOR_REPORT_STATUS, - FREQUENCY_LEVEL, - PD_FREQUENCY_LEVEL, - REPORTABLE_FREQUENCY_LEVEL, - PD_DOCUMENT_TYPE, - PROGRESS_REPORT_STATUS, - PD_STATUS, - RESPONSE_PLAN_TYPE, - OVERALL_STATUS, - PARTNER_PROJECT_STATUS, - PARTNER_ACTIVITY_STATUS, -) -from core.models import ( - Country, - Workspace, - Location, - ResponsePlan, - GatewayType, - CartoDBTable, - PRPRole, -) +from cluster.models import Cluster, ClusterActivity, ClusterObjective +from core.common import (CLUSTER_TYPES, CSO_TYPES, FREQUENCY_LEVEL, + INDICATOR_REPORT_STATUS, OVERALL_STATUS, + PARTNER_ACTIVITY_STATUS, PARTNER_PROJECT_STATUS, + PARTNER_TYPE, PD_DOCUMENT_TYPE, PD_FREQUENCY_LEVEL, + PD_STATUS, PR_ATTACHMENT_TYPES, + PROGRESS_REPORT_STATUS, PRP_ROLE_TYPES, + REPORTABLE_FREQUENCY_LEVEL, REPORTING_TYPES, + RESPONSE_PLAN_TYPE, SHARED_PARTNER_TYPE) from core.countries import COUNTRIES_ALPHA2_CODE, COUNTRIES_ALPHA2_CODE_DICT - +from core.models import (CartoDBTable, Country, GatewayType, Location, PRPRole, + ResponsePlan, Workspace) +from indicator.models import (Disaggregation, DisaggregationValue, + IndicatorBlueprint, IndicatorLocationData, + IndicatorReport, Reportable, + ReportableLocationGoal, ReportingEntity) +from partner.models import (Partner, PartnerActivity, PartnerProject, + PartnerProjectFunding) +from unicef.models import (LowerLevelOutput, PDResultLink, Person, + ProgrammeDocument, ProgressReport, + ProgressReportAttachment, ReportingPeriodDates, + Section) PRP_ROLE_TYPES_LIST = [x[0] for x in PRP_ROLE_TYPES] +PR_ATTACHMENT_TYPES_LIST = [x[0] for x in PR_ATTACHMENT_TYPES] IP_PRP_ROLE_TYPES_LIST = list(filter(lambda item: item.startswith('IP'), PRP_ROLE_TYPES_LIST)) CLUSTER_PRP_ROLE_TYPES_LIST = list(filter(lambda item: item.startswith('CLUSTER'), PRP_ROLE_TYPES_LIST)) PARTNER_PROJECT_STATUS_LIST = [x[0] for x in PARTNER_PROJECT_STATUS] @@ -107,6 +76,8 @@ cs_date_2 = datetime.date(today.year, 3, 24) cs_date_3 = datetime.date(today.year, 5, 15) faker = Faker() +fake_file = ContentFile(bytes(faker.text(), 'utf-8')) +fake_file.name = faker.file_name() def create_fake_multipolygon(): @@ -1248,7 +1219,6 @@ class ProgressReportFactory(factory.django.DjangoModelFactory): reviewed_by_external_id = factory.LazyFunction(lambda: faker.random_number(4, True)) status = fuzzy.FuzzyChoice(PROGRESS_REPORT_STATUS_LIST) review_overall_status = fuzzy.FuzzyChoice(PROGRESS_REPORT_STATUS_LIST) - attachment = None class Meta: django_get_or_create = ( @@ -1257,6 +1227,26 @@ class Meta: model = ProgressReport +class ProgressReportAttachmentFactory(factory.django.DjangoModelFactory): + """ + Arguments: + type {str} -- Attachment type: FACE or Other + progress_report {ProgressReport} -- ProgressReport ORM object to bind + + Ex) ProgressReportAttachmentFactory( + progress_report=progress_report1, + type=PR_ATTACHMENT_TYPES.face, + file=fake_file # ContentFile object with name attribute defined. Make sure file and ORM is deleted after usage. + ) + """ + progress_report = factory.SubFactory('core.factories.ProgressReportFactory', attachment=None) + type = fuzzy.FuzzyChoice(PR_ATTACHMENT_TYPES_LIST) + file = fake_file + + class Meta: + model = ProgressReportAttachment + + class AbstractIndicatorReportFactory(factory.django.DjangoModelFactory): title = factory.LazyAttribute(lambda o: o.reportable.blueprint.title) time_period_start = factory.LazyAttribute(lambda o: o.time_period[0]) diff --git a/django_api/django_api/apps/unicef/tests/test_views.py b/django_api/django_api/apps/unicef/tests/test_views.py index 33056b8e8..3f8c89589 100644 --- a/django_api/django_api/apps/unicef/tests/test_views.py +++ b/django_api/django_api/apps/unicef/tests/test_views.py @@ -1,56 +1,43 @@ import datetime + from django.conf import settings from django.db.models import Q from django.urls import reverse from rest_framework import status -from core.common import ( - INDICATOR_REPORT_STATUS, - OVERALL_STATUS, - PROGRESS_REPORT_STATUS, - PRP_ROLE_TYPES, -) -from core.management.commands._generate_disaggregation_fake_data import ( - generate_3_num_disagg_data, -) -from core.factories import (CartoDBTableFactory, - ProgressReportIndicatorReportFactory, - IPPRPRoleFactory, + +from core.common import (INDICATOR_REPORT_STATUS, OVERALL_STATUS, + PROGRESS_REPORT_STATUS, PRP_ROLE_TYPES, PR_ATTACHMENT_TYPES) +from core.factories import (CartoDBTableFactory, ClusterActivityFactory, + ClusterActivityPartnerActivityFactory, + ClusterFactory, ClusterIndicatorReportFactory, + ClusterObjectiveFactory, ClusterPRPRoleFactory, CountryFactory, DisaggregationFactory, DisaggregationValueFactory, GatewayTypeFactory, + HRReportingPeriodDatesFactory, + IPDisaggregationFactory, IPPRPRoleFactory, LocationFactory, LocationWithReportableLocationGoalFactory, - PartnerUserFactory, PartnerFactory, + LowerLevelOutputFactory, NonPartnerUserFactory, + PartnerFactory, PartnerProjectFactory, + PartnerUserFactory, PDResultLinkFactory, + PersonFactory, ProgrammeDocumentFactory, + ProgressReportAttachmentFactory, ProgressReportFactory, - QuantityReportableToLowerLevelOutputFactory, - QuantityTypeIndicatorBlueprintFactory, - WorkspaceFactory, - SectionFactory, - PersonFactory, - IPDisaggregationFactory, - ProgrammeDocumentFactory, + ProgressReportIndicatorReportFactory, QPRReportingPeriodDatesFactory, - HRReportingPeriodDatesFactory, - PDResultLinkFactory, - LowerLevelOutputFactory, - ClusterPRPRoleFactory, - ResponsePlanFactory, - ClusterFactory, - NonPartnerUserFactory, - ClusterObjectiveFactory, - ClusterActivityFactory, - PartnerProjectFactory, - ClusterActivityPartnerActivityFactory, + QuantityReportableToLowerLevelOutputFactory, QuantityReportableToPartnerActivityFactory, - ClusterIndicatorReportFactory) -from core.tests.base import BaseAPITestCase + QuantityTypeIndicatorBlueprintFactory, + ResponsePlanFactory, SectionFactory, + WorkspaceFactory) +from core.management.commands._generate_disaggregation_fake_data import \ + generate_3_num_disagg_data from core.models import Location +from core.tests.base import BaseAPITestCase from indicator.disaggregators import QuantityIndicatorDisaggregator -from indicator.models import ( - IndicatorReport, - IndicatorBlueprint, - IndicatorLocationData, -) -from unicef.models import ProgressReport +from indicator.models import (IndicatorBlueprint, IndicatorLocationData, + IndicatorReport) +from unicef.models import ProgressReport, ProgressReportAttachment class TestProgrammeDocumentListAPIView(BaseAPITestCase): @@ -131,7 +118,7 @@ def setUp(self): for idx in range(2): qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) - ProgressReportFactory( + pr = ProgressReportFactory( start_date=qpr_period.start_date, end_date=qpr_period.end_date, due_date=qpr_period.due_date, @@ -143,9 +130,14 @@ def setUp(self): submitting_user=self.user, ) + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + for idx in range(6): hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) - ProgressReportFactory( + pr = ProgressReportFactory( start_date=hr_period.start_date, end_date=hr_period.end_date, due_date=hr_period.due_date, @@ -157,6 +149,11 @@ def setUp(self): submitting_user=self.user, ) + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + self.cp_output = PDResultLinkFactory( programme_document=self.pd, ) @@ -234,6 +231,11 @@ def setUp(self): # Logging in as Partner AO self.client.force_authenticate(self.partner_user) + def tearDown(self): + for attachment in ProgressReportAttachment.objects.all(): + attachment.file.delete() + attachment.delete() + def test_list_api(self): url = reverse( 'programme-document', @@ -370,7 +372,7 @@ def setUp(self): for idx in range(2): qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) - ProgressReportFactory( + pr = ProgressReportFactory( start_date=qpr_period.start_date, end_date=qpr_period.end_date, due_date=qpr_period.due_date, @@ -382,9 +384,14 @@ def setUp(self): submitting_user=self.user, ) + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + for idx in range(6): hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) - ProgressReportFactory( + pr = ProgressReportFactory( start_date=hr_period.start_date, end_date=hr_period.end_date, due_date=hr_period.due_date, @@ -396,6 +403,11 @@ def setUp(self): submitting_user=self.user, ) + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + self.cp_output = PDResultLinkFactory( programme_document=self.pd, ) @@ -473,6 +485,11 @@ def setUp(self): # Logging in as Partner AO self.client.force_authenticate(self.partner_user) + def tearDown(self): + for attachment in ProgressReportAttachment.objects.all(): + attachment.file.delete() + attachment.delete() + def test_detail_api(self): url = reverse( 'programme-document-details', @@ -564,7 +581,7 @@ def setUp(self): for idx in range(2): qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) - ProgressReportFactory( + pr = ProgressReportFactory( start_date=qpr_period.start_date, end_date=qpr_period.end_date, due_date=qpr_period.due_date, @@ -576,9 +593,14 @@ def setUp(self): submitting_user=self.user, ) + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + for idx in range(6): hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) - ProgressReportFactory( + pr = ProgressReportFactory( start_date=hr_period.start_date, end_date=hr_period.end_date, due_date=hr_period.due_date, @@ -590,6 +612,11 @@ def setUp(self): submitting_user=self.user, ) + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + self.cp_output = PDResultLinkFactory( programme_document=self.pd, ) @@ -670,6 +697,11 @@ def setUp(self): self.location_id = self.loc1.id self.queryset = self.get_queryset() + def tearDown(self): + for attachment in ProgressReportAttachment.objects.all(): + attachment.file.delete() + attachment.delete() + def get_queryset(self): pd_ids = Location.objects.filter( Q(id=self.location_id) | From 4159e240690921f6ed7f02677dfbfc27c581ce74 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 12:54:26 -0800 Subject: [PATCH 017/817] test: Created TestProgressReportAttachmentListCreateAPIView [#1088] + tearDown method to delete uploaded files + test_list_api + test_create_api --- .../django_api/apps/unicef/serializers.py | 2 - .../apps/unicef/tests/test_views.py | 170 +++++++++++++++++- 2 files changed, 169 insertions(+), 3 deletions(-) diff --git a/django_api/django_api/apps/unicef/serializers.py b/django_api/django_api/apps/unicef/serializers.py index 0c73c3f52..af1871a2a 100644 --- a/django_api/django_api/apps/unicef/serializers.py +++ b/django_api/django_api/apps/unicef/serializers.py @@ -1,8 +1,6 @@ from django.conf import settings -from django.db import transaction from rest_framework import serializers -from rest_framework.exceptions import PermissionDenied, ValidationError from core.serializers import ShortLocationSerializer from utils.filters.constants import Boolean diff --git a/django_api/django_api/apps/unicef/tests/test_views.py b/django_api/django_api/apps/unicef/tests/test_views.py index 3f8c89589..06c1c4c29 100644 --- a/django_api/django_api/apps/unicef/tests/test_views.py +++ b/django_api/django_api/apps/unicef/tests/test_views.py @@ -1,6 +1,10 @@ import datetime +import tempfile +import os from django.conf import settings +from django.core.files import File +from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import Q from django.urls import reverse from rest_framework import status @@ -29,7 +33,7 @@ QuantityReportableToPartnerActivityFactory, QuantityTypeIndicatorBlueprintFactory, ResponsePlanFactory, SectionFactory, - WorkspaceFactory) + WorkspaceFactory, faker) from core.management.commands._generate_disaggregation_fake_data import \ generate_3_num_disagg_data from core.models import Location @@ -790,3 +794,167 @@ def test_list_api_filter_by_due_date(self): self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(len(response.data['results']), len(pr_queryset)) + + +class TestProgressReportAttachmentListCreateAPIView(BaseAPITestCase): + + def setUp(self): + self.country = CountryFactory() + self.workspace = WorkspaceFactory(countries=[self.country, ]) + self.loc_type = GatewayTypeFactory(country=self.country) + self.carto_table = CartoDBTableFactory(location_type=self.loc_type, country=self.country) + self.loc1 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.loc2 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.unicef_officer = PersonFactory() + self.unicef_focal_point = PersonFactory() + self.partner_focal_point = PersonFactory() + self.partner = PartnerFactory(country_code=self.country.country_short_code) + self.partner_user = PartnerUserFactory(partner=self.partner) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, role=PRP_ROLE_TYPES.ip_authorized_officer) + self.sample_disaggregation_value_map = { + "height": ["tall", "medium", "short", "extrashort"], + "age": ["1-2m", "3-4m", "5-6m", '7-10m', '11-13m', '14-16m'], + "gender": ["male", "female", "other"], + } + + self.pd = ProgrammeDocumentFactory( + workspace=self.workspace, + partner=self.partner, + sections=[SectionFactory(), ], + unicef_officers=[self.unicef_officer, ], + unicef_focal_point=[self.unicef_focal_point, ], + partner_focal_point=[self.partner_focal_point, ] + ) + + for idx in range(2): + qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) + pr = ProgressReportFactory( + start_date=qpr_period.start_date, + end_date=qpr_period.end_date, + due_date=qpr_period.due_date, + report_number=idx + 1, + report_type=qpr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + + for idx in range(6): + hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) + pr = ProgressReportFactory( + start_date=hr_period.start_date, + end_date=hr_period.end_date, + due_date=hr_period.due_date, + report_number=idx + 1, + report_type=hr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + + self.cp_output = PDResultLinkFactory( + programme_document=self.pd, + ) + self.llo = LowerLevelOutputFactory( + cp_output=self.cp_output, + ) + self.llo_reportable = QuantityReportableToLowerLevelOutputFactory( + content_object=self.llo, + blueprint=QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + ) + ) + + self.llo_reportable.disaggregations.clear() + + for disagg_name, values in self.sample_disaggregation_value_map.items(): + disagg = IPDisaggregationFactory(name=disagg_name) + + self.llo_reportable.disaggregations.add(disagg) + + for value in values: + DisaggregationValueFactory( + disaggregation=disagg, + value=value + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.llo_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.llo_reportable, + ) + + for pr in self.pd.progress_reports.all(): + ProgressReportIndicatorReportFactory( + progress_report=pr, + reportable=self.llo_reportable, + report_status=INDICATOR_REPORT_STATUS.submitted, + overall_status=OVERALL_STATUS.met, + ) + + # Creating Level-3 disaggregation location data for all locations + generate_3_num_disagg_data(self.llo_reportable, indicator_type="quantity") + + for loc_data in IndicatorLocationData.objects.filter(indicator_report__reportable=self.llo_reportable): + QuantityIndicatorDisaggregator.post_process(loc_data) + + super().setUp() + + # Logging in as Partner AO + self.client.force_authenticate(self.partner_user) + + self.location_id = self.loc1.id + self.pr = self.pd.progress_reports.first() + + settings.MEDIA_ROOT = tempfile.mkdtemp() + + def tearDown(self): + for attachment in ProgressReportAttachment.objects.all(): + attachment.file.delete() + attachment.delete() + + def test_list_api(self): + url = reverse( + 'progress-reports-attachment-list', + kwargs={'workspace_id': self.workspace.id, 'progress_report_id': self.pr.id}) + response = self.client.get(url, format='json') + + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(len(response.data), self.pr.attachments.count()) + + def test_create_api(self): + url = reverse( + 'progress-reports-attachment-list', + kwargs={'workspace_id': self.workspace.id, 'progress_report_id': self.pr.id}) + + f = open('test.txt', 'w') + f.write(faker.text()) + f.close() + + file = File(open('test.txt', 'rb')) + upload_file = SimpleUploadedFile('test', file.read(), content_type="multipart/form-data") + + data = {'type': 'Other', 'path': upload_file} + response = self.client.post(url, data, format="multipart") + + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data['size'], data['path'].size) + + os.remove('test.txt') From a291df45e70b7600eebaf75c372d1b16e1cd369f Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 13:07:47 -0800 Subject: [PATCH 018/817] test: Created TestProgressReportAttachmentAPIView [#1088] + test_detail_api + test_update_api + test_delete_api --- .../apps/unicef/tests/test_views.py | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/django_api/django_api/apps/unicef/tests/test_views.py b/django_api/django_api/apps/unicef/tests/test_views.py index 06c1c4c29..e744f8bb9 100644 --- a/django_api/django_api/apps/unicef/tests/test_views.py +++ b/django_api/django_api/apps/unicef/tests/test_views.py @@ -958,3 +958,175 @@ def test_create_api(self): self.assertEquals(response.data['size'], data['path'].size) os.remove('test.txt') + + +class TestProgressReportAttachmentAPIView(BaseAPITestCase): + + def setUp(self): + self.country = CountryFactory() + self.workspace = WorkspaceFactory(countries=[self.country, ]) + self.loc_type = GatewayTypeFactory(country=self.country) + self.carto_table = CartoDBTableFactory(location_type=self.loc_type, country=self.country) + self.loc1 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.loc2 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.unicef_officer = PersonFactory() + self.unicef_focal_point = PersonFactory() + self.partner_focal_point = PersonFactory() + self.partner = PartnerFactory(country_code=self.country.country_short_code) + self.partner_user = PartnerUserFactory(partner=self.partner) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, role=PRP_ROLE_TYPES.ip_authorized_officer) + self.sample_disaggregation_value_map = { + "height": ["tall", "medium", "short", "extrashort"], + "age": ["1-2m", "3-4m", "5-6m", '7-10m', '11-13m', '14-16m'], + "gender": ["male", "female", "other"], + } + + self.pd = ProgrammeDocumentFactory( + workspace=self.workspace, + partner=self.partner, + sections=[SectionFactory(), ], + unicef_officers=[self.unicef_officer, ], + unicef_focal_point=[self.unicef_focal_point, ], + partner_focal_point=[self.partner_focal_point, ] + ) + + for idx in range(2): + qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) + pr = ProgressReportFactory( + start_date=qpr_period.start_date, + end_date=qpr_period.end_date, + due_date=qpr_period.due_date, + report_number=idx + 1, + report_type=qpr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + + for idx in range(6): + hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) + pr = ProgressReportFactory( + start_date=hr_period.start_date, + end_date=hr_period.end_date, + due_date=hr_period.due_date, + report_number=idx + 1, + report_type=hr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + ProgressReportAttachmentFactory( + progress_report=pr, + type=PR_ATTACHMENT_TYPES.face, + ) + + self.cp_output = PDResultLinkFactory( + programme_document=self.pd, + ) + self.llo = LowerLevelOutputFactory( + cp_output=self.cp_output, + ) + self.llo_reportable = QuantityReportableToLowerLevelOutputFactory( + content_object=self.llo, + blueprint=QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + ) + ) + + self.llo_reportable.disaggregations.clear() + + for disagg_name, values in self.sample_disaggregation_value_map.items(): + disagg = IPDisaggregationFactory(name=disagg_name) + + self.llo_reportable.disaggregations.add(disagg) + + for value in values: + DisaggregationValueFactory( + disaggregation=disagg, + value=value + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.llo_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.llo_reportable, + ) + + for pr in self.pd.progress_reports.all(): + ProgressReportIndicatorReportFactory( + progress_report=pr, + reportable=self.llo_reportable, + report_status=INDICATOR_REPORT_STATUS.submitted, + overall_status=OVERALL_STATUS.met, + ) + + # Creating Level-3 disaggregation location data for all locations + generate_3_num_disagg_data(self.llo_reportable, indicator_type="quantity") + + for loc_data in IndicatorLocationData.objects.filter(indicator_report__reportable=self.llo_reportable): + QuantityIndicatorDisaggregator.post_process(loc_data) + + super().setUp() + + # Logging in as Partner AO + self.client.force_authenticate(self.partner_user) + + self.location_id = self.loc1.id + self.pr = self.pd.progress_reports.first() + self.attachment = self.pr.attachments.first() + + settings.MEDIA_ROOT = tempfile.mkdtemp() + + def tearDown(self): + for attachment in ProgressReportAttachment.objects.all(): + attachment.file.delete() + attachment.delete() + + def test_detail_api(self): + url = reverse( + 'progress-reports-attachment', + kwargs={'workspace_id': self.workspace.id, 'progress_report_id': self.pr.id, 'pk': self.attachment.id}) + response = self.client.get(url, format='json') + + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['id'], self.attachment.id) + + def test_update_api(self): + f = open('test.txt', 'w') + f.write(faker.text() + faker.text() + faker.text()) + f.close() + + file = File(open('test.txt', 'rb')) + upload_file = SimpleUploadedFile('test', file.read(), content_type="multipart/form-data") + + data = {'type': 'Other', 'path': upload_file} + url = reverse( + 'progress-reports-attachment', + kwargs={'workspace_id': self.workspace.id, 'progress_report_id': self.pr.id, 'pk': self.attachment.id}) + response = self.client.put(url, data, format="multipart") + + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['size'], data['path'].size) + + os.remove('test.txt') + + def test_delete_api(self): + url = reverse( + 'progress-reports-attachment', + kwargs={'workspace_id': self.workspace.id, 'progress_report_id': self.pr.id, 'pk': self.attachment.id}) + response = self.client.delete(url, format='multipart') + + self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) From a8b54a7a7b1dabcf801215fce6a16db01ab00c2f Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 14:48:40 -0800 Subject: [PATCH 019/817] feat: Added is_active flag to Location model [#1093] + DB migrations --- .../migrations/0005_location_is_active.py | 20 +++++++++++++++++++ django_api/django_api/apps/core/models.py | 1 + 2 files changed, 21 insertions(+) create mode 100644 django_api/django_api/apps/core/migrations/0005_location_is_active.py diff --git a/django_api/django_api/apps/core/migrations/0005_location_is_active.py b/django_api/django_api/apps/core/migrations/0005_location_is_active.py new file mode 100644 index 000000000..83c38a894 --- /dev/null +++ b/django_api/django_api/apps/core/migrations/0005_location_is_active.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-07 22:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20181024_0034'), + ] + + operations = [ + migrations.AddField( + model_name='location', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/django_api/django_api/apps/core/models.py b/django_api/django_api/apps/core/models.py index 16efb7027..b6f5ec2e8 100644 --- a/django_api/django_api/apps/core/models.py +++ b/django_api/django_api/apps/core/models.py @@ -587,6 +587,7 @@ class Location(MPTTModel): geom = models.MultiPolygonField(null=True, blank=True) point = models.PointField(null=True, blank=True) + is_active = models.BooleanField(default=True) objects = LocationManager() class Meta: From ab1b08ac06d1f71597a5a3867f7fa5bcbb076c35 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 15:14:26 -0800 Subject: [PATCH 020/817] refactor: Moved is_active flag to ReportableLocationGoal instead [#1093] + DB migrations --- django_api/django_api/apps/core/models.py | 1 - .../migrations/0002_reportablelocationgoal_is_active.py} | 6 +++--- django_api/django_api/apps/indicator/models.py | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) rename django_api/django_api/apps/{core/migrations/0005_location_is_active.py => indicator/migrations/0002_reportablelocationgoal_is_active.py} (70%) diff --git a/django_api/django_api/apps/core/models.py b/django_api/django_api/apps/core/models.py index b6f5ec2e8..16efb7027 100644 --- a/django_api/django_api/apps/core/models.py +++ b/django_api/django_api/apps/core/models.py @@ -587,7 +587,6 @@ class Location(MPTTModel): geom = models.MultiPolygonField(null=True, blank=True) point = models.PointField(null=True, blank=True) - is_active = models.BooleanField(default=True) objects = LocationManager() class Meta: diff --git a/django_api/django_api/apps/core/migrations/0005_location_is_active.py b/django_api/django_api/apps/indicator/migrations/0002_reportablelocationgoal_is_active.py similarity index 70% rename from django_api/django_api/apps/core/migrations/0005_location_is_active.py rename to django_api/django_api/apps/indicator/migrations/0002_reportablelocationgoal_is_active.py index 83c38a894..76706bd49 100644 --- a/django_api/django_api/apps/core/migrations/0005_location_is_active.py +++ b/django_api/django_api/apps/indicator/migrations/0002_reportablelocationgoal_is_active.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.20 on 2019-03-07 22:47 +# Generated by Django 1.11.20 on 2019-03-07 23:13 from __future__ import unicode_literals from django.db import migrations, models @@ -8,12 +8,12 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0004_auto_20181024_0034'), + ('indicator', '0001_initial'), ] operations = [ migrations.AddField( - model_name='location', + model_name='reportablelocationgoal', name='is_active', field=models.BooleanField(default=True), ), diff --git a/django_api/django_api/apps/indicator/models.py b/django_api/django_api/apps/indicator/models.py index e320b2fc3..6d890c1ff 100644 --- a/django_api/django_api/apps/indicator/models.py +++ b/django_api/django_api/apps/indicator/models.py @@ -517,6 +517,7 @@ class ReportableLocationGoal(TimeStampedModel): target = JSONField(default=dict([('d', 1), ('v', 0)])) baseline = JSONField(default=dict([('d', 1), ('v', 0)])) in_need = JSONField(blank=True, null=True) + is_active = models.BooleanField(default=True) class Meta: unique_together = ('reportable', 'location') From 287882572544d3a44b6b196eb282d7cb3cf88c94 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 15:48:05 -0800 Subject: [PATCH 021/817] feat: Added de/activating location goal items in PD sync task [#1093] + New location goals are created with is_active flag + After new location goals, update existing loation goals based on location list + Don't overwrite PartnerActivity location goals --- django_api/django_api/apps/unicef/tasks.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/django_api/django_api/apps/unicef/tasks.py b/django_api/django_api/apps/unicef/tasks.py index 8c14474c7..33bc57bdf 100644 --- a/django_api/django_api/apps/unicef/tasks.py +++ b/django_api/django_api/apps/unicef/tasks.py @@ -311,6 +311,9 @@ def process_programme_documents(fast=False, area=False): llos.update(active=False) Reportable.objects.filter(lower_level_outputs__in=llos).update(active=False) + # Mark all ReportableLocationGoal instances referred in LLO Reportables as inactive + ReportableLocationGoal.objects.filter(reportable__lower_level_outputs__in=llos).update(is_active=False) + # Parsing expecting results and set them active, rest will stay inactive for this PD for d in item['expected_results']: # Create PDResultLink @@ -552,6 +555,7 @@ def process_programme_documents(fast=False, area=False): ReportableLocationGoal( reportable=reportable, location=l, + is_active=True, ) for l in Location.objects.filter(id__in=new_locs) ] @@ -561,11 +565,14 @@ def process_programme_documents(fast=False, area=False): ReportableLocationGoal( reportable=reportable, location=l, + is_active=True, ) for l in locations ] ReportableLocationGoal.objects.bulk_create(reportable_location_goals) + ReportableLocationGoal.objects.filter(reportable=reportable, location__in=locations).update(is_active=True) + if partner_activity: # Force update on PA Reportable instance for location update for pa_reportable in partner_activity.reportables.all(): @@ -580,11 +587,15 @@ def process_programme_documents(fast=False, area=False): ReportableLocationGoal( reportable=reportable, location=l, + is_active=True, ) for l in loc_diff ] ReportableLocationGoal.objects.bulk_create(reportable_location_goals) + # We don't overwrite is_active flag on PartnerActivity reportable from LLO locations here + # since Cluster may use those locations + # Check if another page exists if list_data['next']: logger.info("Found new page") From 364fd162040e55d0779f5c8339bfe656d95dfa46 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 7 Mar 2019 16:14:19 -0800 Subject: [PATCH 022/817] refactor: Updated Report generator to look up location goals [#1093] + Only generate IndicatorLocationData object for active ReportableLocationGoal --- django_api/django_api/apps/core/tasks.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/django_api/django_api/apps/core/tasks.py b/django_api/django_api/apps/core/tasks.py index f1ce2ffcd..1bef51ecd 100644 --- a/django_api/django_api/apps/core/tasks.py +++ b/django_api/django_api/apps/core/tasks.py @@ -69,7 +69,7 @@ def process_period_reports(): ): logger.info("Processing Reportable {}".format(reportable)) - if reportable.locations.count() == 0: + if reportable.reportablelocationgoal_set.count() == 0: continue frequency = reportable.frequency @@ -145,14 +145,14 @@ def process_period_reports(): reporting_entity=ReportingEntity.objects.get(title="Cluster"), ) - for location in reportable.locations.all(): + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): logger.info("Creating IndicatorReport {} IndicatorLocationData object {} - {}".format( indicator_report, start_date, end_date )) IndicatorLocationData.objects.create( indicator_report=indicator_report, - location=location, + location=location_goal.location, num_disaggregation=indicator_report.disaggregations.count(), level_reported=indicator_report.disaggregations.count(), disaggregation_reported_on=list(indicator_report.disaggregations.values_list( @@ -180,14 +180,14 @@ def process_period_reports(): reporting_entity=ReportingEntity.objects.get(title="Cluster"), ) - for location in reportable.locations.all(): + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): logger.info("Creating IndicatorReport {} IndicatorLocationData object {} - {}".format( indicator_report, start_date, end_date )) IndicatorLocationData.objects.create( indicator_report=indicator_report, - location=location, + location=location_goal.location, num_disaggregation=indicator_report.disaggregations.count(), level_reported=indicator_report.disaggregations.count(), disaggregation_reported_on=list(indicator_report.disaggregations.values_list( @@ -312,13 +312,13 @@ def create_pr_ir_for_reportable(reportable, pai_ir_for_period, start_date, end_d reporting_entity=ReportingEntity.objects.get(title="UNICEF"), ) - for location in reportable.locations.all(): + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): logger.info("Creating IndicatorReport {} IndicatorLocationData for {} - {}".format( indicator_report, start_date, end_date )) IndicatorLocationData.objects.create( indicator_report=indicator_report, - location=location, + location=location_goal.location, num_disaggregation=indicator_report.disaggregations.count(), level_reported=indicator_report.disaggregations.count(), disaggregation_reported_on=list(indicator_report.disaggregations.values_list( @@ -345,13 +345,13 @@ def create_pr_ir_for_reportable(reportable, pai_ir_for_period, start_date, end_d reporting_entity=ReportingEntity.objects.get(title="UNICEF"), ) - for location in reportable.locations.all(): + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): logger.info("Creating IndicatorReport {} IndicatorLocationData {} - {}".format( indicator_report, start_date, end_date )) IndicatorLocationData.objects.create( indicator_report=indicator_report, - location=location, + location=location_goal.location, num_disaggregation=indicator_report.disaggregations.count(), level_reported=indicator_report.disaggregations.count(), disaggregation_reported_on=list(indicator_report.disaggregations.values_list( From 34968eba564942bf84c4f74943ef0ce0a06bf071 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 7 Mar 2019 17:05:13 -0800 Subject: [PATCH 023/817] feat: Add attachment fields for additional attachments and update labels --- .../ip-reporting/report-attachments.html | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 041f1be21..afa18b522 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -37,7 +37,21 @@ + + + + From bcfd0f0c4a04f64cc341a4a6e1d19d908ad55168 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 7 Mar 2019 17:24:26 -0800 Subject: [PATCH 024/817] feat: Fix label names and add spacing between components --- polymer/src/elements/ip-reporting/report-attachments.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index afa18b522..627825197 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -37,11 +37,12 @@ + + Date: Thu, 7 Mar 2019 17:42:30 -0800 Subject: [PATCH 025/817] feat: Make three different file properties in component to handle uploads --- .../src/elements/ip-reporting/report-attachments.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 627825197..7bca11b22 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -36,7 +36,7 @@ Date: Mon, 11 Mar 2019 13:41:19 -0700 Subject: [PATCH 026/817] fix: Fixed migration merge issue [#1088] + Renumbering on unicef app --- ...ressreportattachment.py => 0008_progressreportattachment.py} | 2 +- .../{0008_auto_20190305_2323.py => 0009_auto_20190305_2323.py} | 2 +- ...t_attachment.py => 0010_remove_progressreport_attachment.py} | 2 +- ...attachment_type.py => 0011_progressreportattachment_type.py} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename django_api/django_api/apps/unicef/migrations/{0007_progressreportattachment.py => 0008_progressreportattachment.py} (96%) rename django_api/django_api/apps/unicef/migrations/{0008_auto_20190305_2323.py => 0009_auto_20190305_2323.py} (96%) rename django_api/django_api/apps/unicef/migrations/{0009_remove_progressreport_attachment.py => 0010_remove_progressreport_attachment.py} (88%) rename django_api/django_api/apps/unicef/migrations/{0010_progressreportattachment_type.py => 0011_progressreportattachment_type.py} (90%) diff --git a/django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py b/django_api/django_api/apps/unicef/migrations/0008_progressreportattachment.py similarity index 96% rename from django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py rename to django_api/django_api/apps/unicef/migrations/0008_progressreportattachment.py index b23f11168..fceb8b711 100644 --- a/django_api/django_api/apps/unicef/migrations/0007_progressreportattachment.py +++ b/django_api/django_api/apps/unicef/migrations/0008_progressreportattachment.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): dependencies = [ - ('unicef', '0006_auto_20181204_0013'), + ('unicef', '0007_auto_20190311_1900'), ] operations = [ diff --git a/django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py b/django_api/django_api/apps/unicef/migrations/0009_auto_20190305_2323.py similarity index 96% rename from django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py rename to django_api/django_api/apps/unicef/migrations/0009_auto_20190305_2323.py index 65230d630..9b7d5e77b 100644 --- a/django_api/django_api/apps/unicef/migrations/0008_auto_20190305_2323.py +++ b/django_api/django_api/apps/unicef/migrations/0009_auto_20190305_2323.py @@ -50,7 +50,7 @@ def reverse_func(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('unicef', '0007_progressreportattachment'), + ('unicef', '0008_progressreportattachment'), ] operations = [ diff --git a/django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py b/django_api/django_api/apps/unicef/migrations/0010_remove_progressreport_attachment.py similarity index 88% rename from django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py rename to django_api/django_api/apps/unicef/migrations/0010_remove_progressreport_attachment.py index 746866b98..0521bceac 100644 --- a/django_api/django_api/apps/unicef/migrations/0009_remove_progressreport_attachment.py +++ b/django_api/django_api/apps/unicef/migrations/0010_remove_progressreport_attachment.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('unicef', '0008_auto_20190305_2323'), + ('unicef', '0009_auto_20190305_2323'), ] operations = [ diff --git a/django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py b/django_api/django_api/apps/unicef/migrations/0011_progressreportattachment_type.py similarity index 90% rename from django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py rename to django_api/django_api/apps/unicef/migrations/0011_progressreportattachment_type.py index ffd8efd29..56fb561cf 100644 --- a/django_api/django_api/apps/unicef/migrations/0010_progressreportattachment_type.py +++ b/django_api/django_api/apps/unicef/migrations/0011_progressreportattachment_type.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ - ('unicef', '0009_remove_progressreport_attachment'), + ('unicef', '0010_remove_progressreport_attachment'), ] operations = [ From 6486277856b0e2f5fcd903179087a737d80f9b57 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 13 Mar 2019 11:34:19 -0700 Subject: [PATCH 027/817] feat: Created 2 python utility functions to delete IndicatorReports and IndicatorLocationData instances [#1094] + delete_all_ilds_for_ir + delete_all_irs_for_pr --- .../django_api/apps/indicator/utilities.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/django_api/django_api/apps/indicator/utilities.py b/django_api/django_api/apps/indicator/utilities.py index 6967de039..62bbdeb07 100644 --- a/django_api/django_api/apps/indicator/utilities.py +++ b/django_api/django_api/apps/indicator/utilities.py @@ -14,3 +14,25 @@ def format_total_value_to_string(total, is_percentage=False): locale = to_locale(get_language()) return formatter(value, locale=locale) + + +def delete_all_ilds_for_ir(indicator_report): + """Delete all IndicatorLocationData instances for given IndicatorReport instance. + + Arguments: + indicator_report {IndicatorReport} -- IndicatorReport instance to delete its location data from + """ + + indicator_report.indicator_location_data.all().delete() + + +def delete_all_irs_for_pr(progress_report): + """Delete all IndicatorReport instances for given ProgressReport instance. + + Arguments: + progress_report {ProgressReport} -- ProgressReport instance to delete its indicator reports from + """ + + for ir in progress_report.indicator_reports.all(): + delete_all_ilds_for_ir(ir) + ir.delete() From 7a0a25dfe96e2adfab9c213082c7d22fbf25020a Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 13 Mar 2019 11:44:48 -0700 Subject: [PATCH 028/817] Render multiple attachments through one component [#1088] --- .../ip-reporting/report-attachments.html | 44 +++++++------------ polymer/src/endpoints.html | 2 +- .../programmeDocumentReportsAttachments.html | 1 + 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 7bca11b22..0dd3eaf16 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -36,26 +36,11 @@ - - - - - - @@ -75,9 +60,7 @@ }, properties: { - faceFiles: Array, - otherFilesOne: Array, - otherFilesTwo: Array, + files: Array, readonly: Boolean, pending: { @@ -85,7 +68,7 @@ statePath: App.Selectors.ProgrammeDocumentReportsAttachments.pending, }, - attachment: { + attachments: { type: Object, statePath: App.Selectors.ProgrammeDocumentReportsAttachments.current, observer: '_setFiles', @@ -136,12 +119,17 @@ }); }, - _setFiles: function (attachment) { - if (attachment && !attachment.path) { - return; - } - - this.set('files', [attachment].filter(Boolean)); + _setFiles: function (attachments) { + attachments.forEach(attachment => { + if(attachment && !attachment.path) return; + }) + this.set('files', attachments.filter(Boolean)); + // if (attachment && !attachment.path) { + // return; + // } + + // this.set('files', [attachment].filter(Boolean)); + // console.log(files) }, _filesChanged: function (change) { diff --git a/polymer/src/endpoints.html b/polymer/src/endpoints.html index ac6c678a0..6859c0dc1 100644 --- a/polymer/src/endpoints.html +++ b/polymer/src/endpoints.html @@ -292,7 +292,7 @@ }, progressReportAttachment: function (locationId, reportId) { - return this.progressReports(locationId) + reportId + '/attachment/'; + return this.progressReports(locationId) + reportId + '/attachments/'; }, analysisOperationalPresence: function (responsePlanId, type) { diff --git a/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html b/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html index 78fc91518..dd0d2f19b 100644 --- a/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html +++ b/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html @@ -17,6 +17,7 @@ } function prop(obj, prop) { + console.log(obj[prop]) return obj[prop]; } From 7daf5c35ecec35cb2dff94f6d15cf8f7f8bfd8ea Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 13 Mar 2019 16:03:52 -0700 Subject: [PATCH 029/817] refactor: Repurposed utilities to include reset logic instead [#1094] + reset_indicator_report_data + reset_progress_report_data --- .../django_api/apps/indicator/utilities.py | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/django_api/django_api/apps/indicator/utilities.py b/django_api/django_api/apps/indicator/utilities.py index 62bbdeb07..bd08d739a 100644 --- a/django_api/django_api/apps/indicator/utilities.py +++ b/django_api/django_api/apps/indicator/utilities.py @@ -2,6 +2,7 @@ from django.utils.translation import to_locale, get_language from indicator.constants import ValueType +from indicator.models import IndicatorLocationData, IndicatorReport def format_total_value_to_string(total, is_percentage=False): @@ -16,23 +17,70 @@ def format_total_value_to_string(total, is_percentage=False): return formatter(value, locale=locale) -def delete_all_ilds_for_ir(indicator_report): - """Delete all IndicatorLocationData instances for given IndicatorReport instance. +def reset_indicator_report_data(indicator_report): + """Delete all IndicatorLocationData instances and generate blank IndicatorLoationData instances for given IndicatorReport instance. Arguments: indicator_report {IndicatorReport} -- IndicatorReport instance to delete its location data from """ + # Reset submission and status attributes + indicator_report.total = {'c': 0, 'd': 0, 'v': 0} + indicator_report.overall_status = "NoS" + indicator_report.report_status = "Due" + indicator_report.submission_date = None + indicator_report.save() + indicator_report.indicator_location_data.all().delete() + reportable = indicator_report.reportable + + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): + IndicatorLocationData.objects.create( + indicator_report=indicator_report, + location=location_goal.location, + num_disaggregation=indicator_report.disaggregations.count(), + level_reported=indicator_report.disaggregations.count(), + disaggregation_reported_on=list(indicator_report.disaggregations.values_list( + 'id', flat=True)), + disaggregation={ + '()': {'c': 0, 'd': 0, 'v': 0} + }, + ) -def delete_all_irs_for_pr(progress_report): - """Delete all IndicatorReport instances for given ProgressReport instance. +def reset_progress_report_data(progress_report): + """Reset all IndicatorReport instances for given ProgressReport instance by deletion and regenerating instances. Arguments: progress_report {ProgressReport} -- ProgressReport instance to delete its indicator reports from """ for ir in progress_report.indicator_reports.all(): - delete_all_ilds_for_ir(ir) + reportable = ir.reportable + time_period_start = ir.time_period_start + time_period_end = ir.time_period_end + due_date = ir.due_date + title = ir.title + total = {'c': 0, 'd': 0, 'v': 0} + overall_status = "NoS" + report_status = "Due" + submission_date = None + reporting_entity = ir.reporting_entity + + # Delete current indicator report and its indicator location data will be deleted in cascade ir.delete() + + indicator_report = IndicatorReport.objects.create( + reportable=reportable, + time_period_start=time_period_start, + time_period_end=time_period_end, + due_date=due_date, + title=title, + total=total, + overall_status=overall_status, + report_status=report_status, + submission_date=submission_date, + reporting_entity=reporting_entity, + ) + + reset_indicator_report_data(indicator_report) From 7b1574dd73f018305061489f5643dc0b85102d89 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 13 Mar 2019 16:04:55 -0700 Subject: [PATCH 030/817] feat: Created ReportRefreshAPIView endpoint [#1094] + APIView class + URL pattern + ReportRefreshSerializer --- .../django_api/apps/indicator/serializers.py | 5 +++ django_api/django_api/apps/indicator/urls.py | 5 +++ django_api/django_api/apps/indicator/views.py | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/django_api/django_api/apps/indicator/serializers.py b/django_api/django_api/apps/indicator/serializers.py index 31421eace..8fa1c7c56 100644 --- a/django_api/django_api/apps/indicator/serializers.py +++ b/django_api/django_api/apps/indicator/serializers.py @@ -504,6 +504,11 @@ class ReportableReportingFrequencyIdSerializer(serializers.Serializer): ) +class ReportRefreshSerializer(serializers.Serializer): + report_id = serializers.IntegerField(min_value=1) + report_type = serializers.ChoiceField(choices=['PR', 'IR']) + + class SimpleIndicatorLocationDataListSerializer(serializers.ModelSerializer): location = LocationSerializer(read_only=True) diff --git a/django_api/django_api/apps/indicator/urls.py b/django_api/django_api/apps/indicator/urls.py index 94b9afd16..b4408f584 100644 --- a/django_api/django_api/apps/indicator/urls.py +++ b/django_api/django_api/apps/indicator/urls.py @@ -16,6 +16,7 @@ ReportableLocationGoalBaselineInNeedAPIView, ClusterIndicatorSendIMOMessageAPIView, ReportableReportingFrequencyListAPIView, + ReportRefreshAPIView, ) @@ -74,4 +75,8 @@ url(r'^reporting-frequencies/$', ReportableReportingFrequencyListAPIView.as_view(), name='reportable-reporting-frequency-list-api'), + + url(r'^report-refresh/$', + ReportRefreshAPIView.as_view(), + name='report-refresh-api'), ] diff --git a/django_api/django_api/apps/indicator/views.py b/django_api/django_api/apps/indicator/views.py index d8984ac64..4c589ca12 100644 --- a/django_api/django_api/apps/indicator/views.py +++ b/django_api/django_api/apps/indicator/views.py @@ -20,6 +20,8 @@ AnyPermission, IsUNICEFAPIUser, IsAuthenticated, + IsPartnerAuthorizedOfficerForCurrentWorkspace, + IsPartnerEditorForCurrentWorkspace, HasAnyRole, ) from core.paginations import SmallPagination @@ -54,6 +56,7 @@ DisaggregationListSerializer, IndicatorReportReviewSerializer, IndicatorReportSimpleSerializer, + ReportRefreshSerializer, ReportableLocationGoalBaselineInNeedSerializer, ClusterIndicatorIMOMessageSerializer, ReportableReportingFrequencyIdSerializer, @@ -67,6 +70,7 @@ Disaggregation, ReportableLocationGoal ) +from .utilities import delete_all_ilds_for_ir, delete_all_irs_for_pr from functools import reduce logger = logging.getLogger(__name__) @@ -828,3 +832,30 @@ def post(self, request, *args, **kwargs): response.append({'frequency': freq, 'cs_dates': cs_dates}) return Response(response, status=status.HTTP_200_OK) + + +class ReportRefreshAPIView(APIView): + + permission_classes = ( + AnyPermission( + IsPartnerAuthorizedOfficerForCurrentWorkspace, + IsPartnerEditorForCurrentWorkspace), + ) + + @transaction.atomic + def post(self, request, *args, **kwargs): + """ + Removes all IndicatorReport instances for given ProgressReport, including underlying IndicatorLocationData instances + """ + serializer = ReportRefreshSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + if serializer.validated_data['report_type'] == 'PR': + report = get_object_or_404(ProgressReport, id=serializer.validated_data['report_id']) + else: + report = get_object_or_404(IndicatorReport, id=serializer.validated_data['report_id']) + + if report.progress_report: + raise ValidationError("This indicator report is linked to a progress report. Use the progress report ID instead.") + + return Response({"response": "OK"}, status=status.HTTP_200_OK) From ef8487abd615ffff4088743efd616c9513f72e7f Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 13 Mar 2019 16:23:02 -0700 Subject: [PATCH 031/817] Split attachments into separate components again and verify files render [#1088] --- .../ip-reporting/report-attachments.html | 46 +++++++++++++------ polymer/src/endpoints.html | 2 +- .../programmeDocumentReportsAttachments.html | 1 - 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 0dd3eaf16..6131816f5 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -36,11 +36,26 @@ + + + + + + @@ -60,7 +75,9 @@ }, properties: { - files: Array, + faceFile: Object, + otherOne: Object, + otherTwo: Object, readonly: Boolean, pending: { @@ -69,7 +86,7 @@ }, attachments: { - type: Object, + type: Array, statePath: App.Selectors.ProgrammeDocumentReportsAttachments.current, observer: '_setFiles', }, @@ -95,7 +112,7 @@ ], _computeAttachmentsUrl: function (locationId, reportId) { - return App.Endpoints.progressReportAttachment(locationId, reportId); + return App.Endpoints.progressReportAttachments(locationId, reportId); }, _onDeleteFile: function (e) { @@ -120,16 +137,17 @@ }, _setFiles: function (attachments) { + console.log(attachments) attachments.forEach(attachment => { + if(attachment && !attachment.path) return; - }) - this.set('files', attachments.filter(Boolean)); - // if (attachment && !attachment.path) { - // return; - // } - - // this.set('files', [attachment].filter(Boolean)); - // console.log(files) + + if(attachment.type === 'FACE') this.set('faceFile', [attachment].filter(Boolean)); + + if(attachment.type === 'Other') { + this.otherOne.length === 0 ? this.set('otherOne', [attachment].filter(Boolean)) : this.set('otherTwo', [attachment].filter(Boolean)); + }; + }); }, _filesChanged: function (change) { diff --git a/polymer/src/endpoints.html b/polymer/src/endpoints.html index 6859c0dc1..0faf17aa7 100644 --- a/polymer/src/endpoints.html +++ b/polymer/src/endpoints.html @@ -291,7 +291,7 @@ return this._buildUrl('/account/user-profile/'); }, - progressReportAttachment: function (locationId, reportId) { + progressReportAttachments: function (locationId, reportId) { return this.progressReports(locationId) + reportId + '/attachments/'; }, diff --git a/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html b/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html index dd0d2f19b..78fc91518 100644 --- a/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html +++ b/polymer/src/redux/selectors/programmeDocumentReportsAttachments.html @@ -17,7 +17,6 @@ } function prop(obj, prop) { - console.log(obj[prop]) return obj[prop]; } From 94b7490765359abba7035dfb9512850d94cadcb0 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 15:39:58 -0700 Subject: [PATCH 032/817] refactor: Extracted closure functions for report generator into helpers module [#1094] + Refactored closures to import from helpers --- django_api/django_api/apps/core/helpers.py | 278 +++++++++++++++++++++ django_api/django_api/apps/core/tasks.py | 264 +------------------ 2 files changed, 282 insertions(+), 260 deletions(-) diff --git a/django_api/django_api/apps/core/helpers.py b/django_api/django_api/apps/core/helpers.py index ef59e6b9d..fac909a76 100644 --- a/django_api/django_api/apps/core/helpers.py +++ b/django_api/django_api/apps/core/helpers.py @@ -1,5 +1,6 @@ import math import random +import logging import sys import os from ast import literal_eval @@ -13,6 +14,17 @@ PD_FREQUENCY_LEVEL, ) +from indicator.models import ( + IndicatorBlueprint, + IndicatorReport, + IndicatorLocationData, + ReportingEntity, + Reportable, +) +from unicef.models import ProgressReport + +logger = logging.getLogger(__name__) + @contextmanager def suppress_stdout(): @@ -338,3 +350,269 @@ def find_missing_frequency_period_dates_for_indicator_report(indicator, latest_i date_list.append(missing_date) return date_list + + +def get_latest_pr_by_type(pd, report_type): + """ + Return latest ProgressReport instance given report_type + + Arguments: + report_type {str} -- A report type as string: [QPR, HR, SR] + + Returns: + ProgressReport -- Latest ProgressReport instance for given report_type + """ + + if report_type == "QPR": + return pd.progress_reports \ + .filter(report_type="QPR").order_by('start_date').last() + + if report_type == "HR": + return pd.progress_reports \ + .filter(report_type="HR").order_by('start_date').last() + + if report_type == "SR": + return pd.progress_reports \ + .filter(report_type="SR").order_by('due_date').last() + + +def create_pr_for_report_type(pd, idx, reporting_period, generate_from_date): + """ + Create ProgressReport instance by its ReportingPeriodDate instance's report type + + Arguments: + pd {ProgrammeDocument} -- ProgrammeDocument instance for ProgressReport to generate + idx {int} -- Integer to denote report number + reporting_period {ReportingPeriodDates} -- ReportingPeriodDates instance for new ProgressReport + generate_from_date {datetime.datetime} -- datetime instance from latest ProgressReport on same report + + Returns: + Tuple[ProgressReport, datetime.datetime, datetime.datetime, datetime.datetime] + - Newly generated ProgressReport & 3 datetime objects + """ + + end_date = reporting_period.end_date + due_date = reporting_period.due_date + start_date = reporting_period.start_date + + # Create ProgressReport first + logger.info("Creating ProgressReport for {} - {}".format(start_date, end_date)) + + # Re-query latest ProgressReport by report type + latest_progress_report = get_latest_pr_by_type(pd, reporting_period.report_type) + + if latest_progress_report: + report_type = latest_progress_report.report_type + report_number = latest_progress_report.report_number + 1 + is_final = idx == pd.reporting_periods.filter(report_type=reporting_period.report_type).count() - 1 + + else: + report_number = 1 + report_type = reporting_period.report_type + is_final = False + + next_progress_report = ProgressReport.objects.create( + start_date=start_date, + end_date=end_date, + due_date=due_date, + programme_document=pd, + report_type=report_type, + report_number=report_number, + is_final=is_final, + ) + + return (next_progress_report, start_date, end_date, due_date) + + +def create_pr_ir_for_reportable(pd, reportable, pai_ir_for_period, start_date, end_date, due_date): + if reportable.blueprint.unit == IndicatorBlueprint.NUMBER: + logger.info("Creating Quantity IndicatorReport for {} - {}".format(start_date, end_date)) + indicator_report = IndicatorReport.objects.create( + progress_report=None, + reportable=reportable, + parent=pai_ir_for_period, + time_period_start=start_date, + time_period_end=end_date, + due_date=due_date, + title=reportable.blueprint.title, + total={'c': 0, 'd': 0, 'v': 0}, + overall_status="NoS", + report_status="Due", + submission_date=None, + reporting_entity=ReportingEntity.objects.get(title="UNICEF"), + ) + + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): + logger.info("Creating IndicatorReport {} IndicatorLocationData for {} - {}".format( + indicator_report, start_date, end_date + )) + IndicatorLocationData.objects.create( + indicator_report=indicator_report, + location=location_goal.location, + num_disaggregation=indicator_report.disaggregations.count(), + level_reported=indicator_report.disaggregations.count(), + disaggregation_reported_on=list(indicator_report.disaggregations.values_list( + 'id', flat=True)), + disaggregation={ + '()': {'c': 0, 'd': 0, 'v': 0} + }, + ) + + else: + logger.info("Creating PD {} Ratio IndicatorReport for {} - {}".format(pd, start_date, end_date)) + indicator_report = IndicatorReport.objects.create( + progress_report=None, + reportable=reportable, + parent=pai_ir_for_period, + time_period_start=start_date, + time_period_end=end_date, + due_date=due_date, + title=reportable.blueprint.title, + total={'c': 0, 'd': 0, 'v': 0}, + overall_status="NoS", + report_status="Due", + submission_date=None, + reporting_entity=ReportingEntity.objects.get(title="UNICEF"), + ) + + for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): + logger.info("Creating IndicatorReport {} IndicatorLocationData {} - {}".format( + indicator_report, start_date, end_date + )) + IndicatorLocationData.objects.create( + indicator_report=indicator_report, + location=location_goal.location, + num_disaggregation=indicator_report.disaggregations.count(), + level_reported=indicator_report.disaggregations.count(), + disaggregation_reported_on=list(indicator_report.disaggregations.values_list( + 'id', flat=True)), + disaggregation={ + '()': {'c': 0, 'd': 0, 'v': 0} + }, + ) + + return indicator_report + + +def create_ir_and_ilds_for_pr(pd, reportable_queryset, next_progress_report, start_date, end_date, due_date): + """ + Create a set of new IndicatorReports and IndicatorLocationData instances per + IndicatorReport instance, with passed-in new dates and new ProgressReport instance + Arguments: + pd {ProgrammeDocument} -- ProgrammeDocument instnace + reportable_queryset {django.Queryset[Reportable]} -- Reportable queryset on LLO + next_progress_report {ProgressReport} -- Newly generated Progress Report instance + start_date {datetime.datetime} -- Start date for reporting + end_date {datetime.datetime} -- End date for reporting + due_date {datetime.datetime} -- due date for reporting + """ + + if next_progress_report.report_type != "SR": + if next_progress_report.report_type == "QPR": + queryset = reportable_queryset + + else: + # Filter non-Cluster reportables first + queryset = reportable_queryset.filter( + ca_indicator_used_by_reporting_entity__isnull=True, + is_unicef_hf_indicator=True + ) + + ir_list = list() + + for reportable in queryset: + indicator_report = create_pr_ir_for_reportable( + reportable, + None, + start_date, + end_date, + due_date, + ) + indicator_report.progress_report = next_progress_report + indicator_report.save() + ir_list.append(indicator_report) + + if next_progress_report.report_type == "HR": + hr_reports = list() + + # If there are no UNICEF HF indicator reports then delete blank ProgressReport + if len(ir_list) == 0: + # Re-assign report_number to new HR + report_number = next_progress_report.report_number + next_progress_report.delete() + + else: + # Pre-populate new HR report_number in case a new Progress Report needs to be generated + report_number = next_progress_report.report_number + 1 + hr_reports.append(next_progress_report) + + # Process cluster Reportables separately + for reportable in reportable_queryset.filter(ca_indicator_used_by_reporting_entity__isnull=False): + cai_indicator = reportable.ca_indicator_used_by_reporting_entity + pai_ir_for_period = None + + # If LLO indicator has ClusterActivity Indicator ID reference, + # find the adopted PartnerActivity indicator from ClusterActivity Indicator + # with LLO's Partner ID + # and grab a corresponding IndicatorReport from ClusterActivity Indicator + # given the start & end date + if cai_indicator: + try: + # Grabbing first adopted partner activity in case + # multiple adopted partner activities happen, although this is illegal state! + pai_indicator = cai_indicator.children \ + .filter(partner_activities__partner=pd.partner) \ + .first() + pai_ir_for_period = pai_indicator.indicator_reports.get( + time_period_start=start_date, + time_period_end=end_date, + ) + + if pai_ir_for_period: + indicator_report = create_pr_ir_for_reportable( + reportable, + pai_ir_for_period, + pai_ir_for_period.time_period_start, + pai_ir_for_period.time_period_end, + pai_ir_for_period.due_date, + ) + + # Bundle this cluster LLO Indicator report to HR progress report generated so far + # for this iteration if the dates are matching + for hr_report in hr_reports: + if indicator_report.time_period_start == hr_report.start_date \ + and indicator_report.time_period_end == hr_report.end_date \ + and indicator_report.due_date == hr_report.due_date: + indicator_report.progress_report = hr_report + break + + if not indicator_report.progress_report: + # Otherwise, create a brand new HR progress report + # for this cluster LLO Indicator report + new_cluster_hr_progress_report = ProgressReport.objects.create( + start_date=indicator_report.time_period_start, + end_date=indicator_report.time_period_end, + due_date=indicator_report.due_date, + programme_document=pd, + report_type="HR", + report_number=report_number, + is_final=False, + ) + indicator_report.progress_report = new_cluster_hr_progress_report + + # Increment report_number for next HR progress report to be created if needed + report_number += 1 + hr_reports.append(new_cluster_hr_progress_report) + + indicator_report.save() + + except Reportable.DoesNotExist as e: + logger.exception( + "FAILURE: CANNOT FIND adopted PartnerActivity Reportable " + "from given ClusterActivity Reportable and PD Partner ID. " + "Skipping link!", e) + except IndicatorReport.DoesNotExist as e: + logger.exception( + "FAILURE: CANNOT FIND IndicatorReport from adopted PartnerActivity Reportable " + "linked with LLO Reportable. " + "Skipping link!", e) diff --git a/django_api/django_api/apps/core/tasks.py b/django_api/django_api/apps/core/tasks.py index 1bef51ecd..1746b03db 100644 --- a/django_api/django_api/apps/core/tasks.py +++ b/django_api/django_api/apps/core/tasks.py @@ -16,8 +16,11 @@ from core.helpers import ( calculate_end_date_given_start_date, find_missing_frequency_period_dates_for_indicator_report, + get_latest_pr_by_type, + create_pr_for_report_type, + create_ir_and_ilds_for_pr, ) -from unicef.models import ProgrammeDocument, ProgressReport +from unicef.models import ProgrammeDocument from indicator.models import Reportable, IndicatorBlueprint, IndicatorReport, ReportingEntity, IndicatorLocationData @@ -197,29 +200,6 @@ def process_period_reports(): }, ) - def get_latest_pr_by_type(pd, report_type): - """ - Return latest ProgressReport instance given report_type - - Arguments: - report_type {str} -- A report type as string: [QPR, HR, SR] - - Returns: - ProgressReport -- Latest ProgressReport instance for given report_type - """ - - if report_type == "QPR": - return pd.progress_reports \ - .filter(report_type="QPR").order_by('start_date').last() - - if report_type == "HR": - return pd.progress_reports \ - .filter(report_type="HR").order_by('start_date').last() - - if report_type == "SR": - return pd.progress_reports \ - .filter(report_type="SR").order_by('due_date').last() - # PD report generation for pd in ProgrammeDocument.objects.filter(status=PD_STATUS.active): logger.info("\nProcessing ProgrammeDocument {}".format(pd.id)) @@ -249,242 +229,6 @@ def get_latest_pr_by_type(pd, report_type): logger.info("Last HR report: %s for PD %s" % (generate_from_date_hr, pd)) logger.info("Last SR report: %s for PD %s" % (generate_from_date_sr, pd)) - def create_pr_for_report_type(reporting_period, generate_from_date): - """ - Create ProgressReport instance by its ReportingPeriodDate instance's report type - - Arguments: - reporting_period {ReportingPeriodDates} -- ReportingPeriodDates instance for new ProgressReport - generate_from_date {datetime.datetime} -- datetime instance from latest ProgressReport on same report - - Returns: - Tuple[ProgressReport, datetime.datetime, datetime.datetime, datetime.datetime] - - Newly generated ProgressReport & 3 datetime objects - """ - - end_date = reporting_period.end_date - due_date = reporting_period.due_date - start_date = reporting_period.start_date - - # Create ProgressReport first - logger.info("Creating ProgressReport for {} - {}".format(start_date, end_date)) - - # Re-query latest ProgressReport by report type - latest_progress_report = get_latest_pr_by_type(pd, reporting_period.report_type) - - if latest_progress_report: - report_type = latest_progress_report.report_type - report_number = latest_progress_report.report_number + 1 - is_final = idx == pd.reporting_periods.filter(report_type=reporting_period.report_type).count() - 1 - - else: - report_number = 1 - report_type = reporting_period.report_type - is_final = False - - next_progress_report = ProgressReport.objects.create( - start_date=start_date, - end_date=end_date, - due_date=due_date, - programme_document=pd, - report_type=report_type, - report_number=report_number, - is_final=is_final, - ) - - return (next_progress_report, start_date, end_date, due_date) - - def create_pr_ir_for_reportable(reportable, pai_ir_for_period, start_date, end_date, due_date): - if reportable.blueprint.unit == IndicatorBlueprint.NUMBER: - logger.info("Creating Quantity IndicatorReport for {} - {}".format(start_date, end_date)) - indicator_report = IndicatorReport.objects.create( - progress_report=None, - reportable=reportable, - parent=pai_ir_for_period, - time_period_start=start_date, - time_period_end=end_date, - due_date=due_date, - title=reportable.blueprint.title, - total={'c': 0, 'd': 0, 'v': 0}, - overall_status="NoS", - report_status="Due", - submission_date=None, - reporting_entity=ReportingEntity.objects.get(title="UNICEF"), - ) - - for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): - logger.info("Creating IndicatorReport {} IndicatorLocationData for {} - {}".format( - indicator_report, start_date, end_date - )) - IndicatorLocationData.objects.create( - indicator_report=indicator_report, - location=location_goal.location, - num_disaggregation=indicator_report.disaggregations.count(), - level_reported=indicator_report.disaggregations.count(), - disaggregation_reported_on=list(indicator_report.disaggregations.values_list( - 'id', flat=True)), - disaggregation={ - '()': {'c': 0, 'd': 0, 'v': 0} - }, - ) - - else: - logger.info("Creating PD {} Ratio IndicatorReport for {} - {}".format(pd, start_date, end_date)) - indicator_report = IndicatorReport.objects.create( - progress_report=None, - reportable=reportable, - parent=pai_ir_for_period, - time_period_start=start_date, - time_period_end=end_date, - due_date=due_date, - title=reportable.blueprint.title, - total={'c': 0, 'd': 0, 'v': 0}, - overall_status="NoS", - report_status="Due", - submission_date=None, - reporting_entity=ReportingEntity.objects.get(title="UNICEF"), - ) - - for location_goal in reportable.reportablelocationgoal_set.filter(is_active=True): - logger.info("Creating IndicatorReport {} IndicatorLocationData {} - {}".format( - indicator_report, start_date, end_date - )) - IndicatorLocationData.objects.create( - indicator_report=indicator_report, - location=location_goal.location, - num_disaggregation=indicator_report.disaggregations.count(), - level_reported=indicator_report.disaggregations.count(), - disaggregation_reported_on=list(indicator_report.disaggregations.values_list( - 'id', flat=True)), - disaggregation={ - '()': {'c': 0, 'd': 0, 'v': 0} - }, - ) - - return indicator_report - - def create_ir_and_ilds_for_pr(reportable_queryset, next_progress_report, start_date, end_date, due_date): - """ - Create a set of new IndicatorReports and IndicatorLocationData instances per - IndicatorReport instance, with passed-in new dates and new ProgressReport instance - Arguments: - reportable_queryset {django.Queryset[Reportable]} -- Reportable queryset on LLO - next_progress_report {ProgressReport} -- Newly generated Progress Report instance - start_date {datetime.datetime} -- Start date for reporting - end_date {datetime.datetime} -- End date for reporting - due_date {datetime.datetime} -- due date for reporting - """ - - if next_progress_report.report_type != "SR": - if next_progress_report.report_type == "QPR": - queryset = reportable_queryset - - else: - # Filter non-Cluster reportables first - queryset = reportable_queryset.filter( - ca_indicator_used_by_reporting_entity__isnull=True, - is_unicef_hf_indicator=True - ) - - ir_list = list() - - for reportable in queryset: - indicator_report = create_pr_ir_for_reportable( - reportable, - None, - start_date, - end_date, - due_date, - ) - indicator_report.progress_report = next_progress_report - indicator_report.save() - ir_list.append(indicator_report) - - if next_progress_report.report_type == "HR": - hr_reports = list() - - # If there are no UNICEF HF indicator reports then delete blank ProgressReport - if len(ir_list) == 0: - # Re-assign report_number to new HR - report_number = next_progress_report.report_number - next_progress_report.delete() - - else: - # Pre-populate new HR report_number in case a new Progress Report needs to be generated - report_number = next_progress_report.report_number + 1 - hr_reports.append(next_progress_report) - - # Process cluster Reportables separately - for reportable in reportable_queryset.filter(ca_indicator_used_by_reporting_entity__isnull=False): - cai_indicator = reportable.ca_indicator_used_by_reporting_entity - pai_ir_for_period = None - - # If LLO indicator has ClusterActivity Indicator ID reference, - # find the adopted PartnerActivity indicator from ClusterActivity Indicator - # with LLO's Partner ID - # and grab a corresponding IndicatorReport from ClusterActivity Indicator - # given the start & end date - if cai_indicator: - try: - # Grabbing first adopted partner activity in case - # multiple adopted partner activities happen, although this is illegal state! - pai_indicator = cai_indicator.children \ - .filter(partner_activities__partner=pd.partner) \ - .first() - pai_ir_for_period = pai_indicator.indicator_reports.get( - time_period_start=start_date, - time_period_end=end_date, - ) - - if pai_ir_for_period: - indicator_report = create_pr_ir_for_reportable( - reportable, - pai_ir_for_period, - pai_ir_for_period.time_period_start, - pai_ir_for_period.time_period_end, - pai_ir_for_period.due_date, - ) - - # Bundle this cluster LLO Indicator report to HR progress report generated so far - # for this iteration if the dates are matching - for hr_report in hr_reports: - if indicator_report.time_period_start == hr_report.start_date \ - and indicator_report.time_period_end == hr_report.end_date \ - and indicator_report.due_date == hr_report.due_date: - indicator_report.progress_report = hr_report - break - - if not indicator_report.progress_report: - # Otherwise, create a brand new HR progress report - # for this cluster LLO Indicator report - new_cluster_hr_progress_report = ProgressReport.objects.create( - start_date=indicator_report.time_period_start, - end_date=indicator_report.time_period_end, - due_date=indicator_report.due_date, - programme_document=pd, - report_type="HR", - report_number=report_number, - is_final=False, - ) - indicator_report.progress_report = new_cluster_hr_progress_report - - # Increment report_number for next HR progress report to be created if needed - report_number += 1 - hr_reports.append(new_cluster_hr_progress_report) - - indicator_report.save() - - except Reportable.DoesNotExist as e: - logger.exception( - "FAILURE: CANNOT FIND adopted PartnerActivity Reportable " - "from given ClusterActivity Reportable and PD Partner ID. " - "Skipping link!", e) - except IndicatorReport.DoesNotExist as e: - logger.exception( - "FAILURE: CANNOT FIND IndicatorReport from adopted PartnerActivity Reportable " - "linked with LLO Reportable. " - "Skipping link!", e) - with transaction.atomic(): # Handling QPR reporting periods for idx, reporting_period in enumerate(pd.reporting_periods.filter(report_type="QPR").order_by( From c630b528e467df2613ff6b79d77d6e11e1362bb4 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 16:08:49 -0700 Subject: [PATCH 033/817] feat: Updated reset_progress_report_data to use helper function [#1094] + Regenerate Indicator Reports with dual reporting in mind + Hooked up ReportRefreshAPIView --- django_api/django_api/apps/core/helpers.py | 2 +- .../django_api/apps/indicator/utilities.py | 53 +++++++++---------- django_api/django_api/apps/indicator/views.py | 5 +- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/django_api/django_api/apps/core/helpers.py b/django_api/django_api/apps/core/helpers.py index fac909a76..ea76c00a6 100644 --- a/django_api/django_api/apps/core/helpers.py +++ b/django_api/django_api/apps/core/helpers.py @@ -23,7 +23,7 @@ ) from unicef.models import ProgressReport -logger = logging.getLogger(__name__) +logger = logging.getLogger("django") @contextmanager diff --git a/django_api/django_api/apps/indicator/utilities.py b/django_api/django_api/apps/indicator/utilities.py index bd08d739a..cfae7476c 100644 --- a/django_api/django_api/apps/indicator/utilities.py +++ b/django_api/django_api/apps/indicator/utilities.py @@ -1,8 +1,16 @@ from babel.numbers import format_number, format_percent + from django.utils.translation import to_locale, get_language +from django.db import transaction + +from core.helpers import ( + create_ir_and_ilds_for_pr, +) from indicator.constants import ValueType -from indicator.models import IndicatorLocationData, IndicatorReport +from indicator.models import ( + IndicatorLocationData, +) def format_total_value_to_string(total, is_percentage=False): @@ -55,32 +63,19 @@ def reset_progress_report_data(progress_report): progress_report {ProgressReport} -- ProgressReport instance to delete its indicator reports from """ - for ir in progress_report.indicator_reports.all(): - reportable = ir.reportable - time_period_start = ir.time_period_start - time_period_end = ir.time_period_end - due_date = ir.due_date - title = ir.title - total = {'c': 0, 'd': 0, 'v': 0} - overall_status = "NoS" - report_status = "Due" - submission_date = None - reporting_entity = ir.reporting_entity - - # Delete current indicator report and its indicator location data will be deleted in cascade - ir.delete() - - indicator_report = IndicatorReport.objects.create( - reportable=reportable, - time_period_start=time_period_start, - time_period_end=time_period_end, - due_date=due_date, - title=title, - total=total, - overall_status=overall_status, - report_status=report_status, - submission_date=submission_date, - reporting_entity=reporting_entity, - ) + # Delete all current indicator reports and their indicator location data will be deleted in cascade + progress_report.indicator_reports.all().delete() - reset_indicator_report_data(indicator_report) + pd = progress_report.programme_document + + # Get Active LLO indicators only + reportable_queryset = pd.reportable_queryset.filter(active=True) + + with transaction.atomic(): + create_ir_and_ilds_for_pr( + reportable_queryset, + progress_report, + progress_report.start_date, + progress_report.end_date, + progress_report.due_date + ) diff --git a/django_api/django_api/apps/indicator/views.py b/django_api/django_api/apps/indicator/views.py index 4c589ca12..42f9418ee 100644 --- a/django_api/django_api/apps/indicator/views.py +++ b/django_api/django_api/apps/indicator/views.py @@ -70,7 +70,7 @@ Disaggregation, ReportableLocationGoal ) -from .utilities import delete_all_ilds_for_ir, delete_all_irs_for_pr +from .utilities import reset_indicator_report_data, reset_progress_report_data from functools import reduce logger = logging.getLogger(__name__) @@ -852,10 +852,13 @@ def post(self, request, *args, **kwargs): if serializer.validated_data['report_type'] == 'PR': report = get_object_or_404(ProgressReport, id=serializer.validated_data['report_id']) + reset_progress_report_data(report) else: report = get_object_or_404(IndicatorReport, id=serializer.validated_data['report_id']) if report.progress_report: raise ValidationError("This indicator report is linked to a progress report. Use the progress report ID instead.") + reset_indicator_report_data(report) + return Response({"response": "OK"}, status=status.HTTP_200_OK) From f6bed03086fab3f921662f52afbca003bdc98c14 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 16:17:18 -0700 Subject: [PATCH 034/817] fix: Resolved circular dependencies in helpers [#1094] --- django_api/django_api/apps/core/helpers.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/django_api/django_api/apps/core/helpers.py b/django_api/django_api/apps/core/helpers.py index ea76c00a6..9614fe72d 100644 --- a/django_api/django_api/apps/core/helpers.py +++ b/django_api/django_api/apps/core/helpers.py @@ -14,13 +14,6 @@ PD_FREQUENCY_LEVEL, ) -from indicator.models import ( - IndicatorBlueprint, - IndicatorReport, - IndicatorLocationData, - ReportingEntity, - Reportable, -) from unicef.models import ProgressReport logger = logging.getLogger("django") @@ -425,6 +418,13 @@ def create_pr_for_report_type(pd, idx, reporting_period, generate_from_date): def create_pr_ir_for_reportable(pd, reportable, pai_ir_for_period, start_date, end_date, due_date): + from indicator.models import ( + IndicatorBlueprint, + IndicatorReport, + IndicatorLocationData, + ReportingEntity, + ) + if reportable.blueprint.unit == IndicatorBlueprint.NUMBER: logger.info("Creating Quantity IndicatorReport for {} - {}".format(start_date, end_date)) indicator_report = IndicatorReport.objects.create( @@ -506,6 +506,10 @@ def create_ir_and_ilds_for_pr(pd, reportable_queryset, next_progress_report, sta end_date {datetime.datetime} -- End date for reporting due_date {datetime.datetime} -- due date for reporting """ + from indicator.models import ( + IndicatorReport, + Reportable, + ) if next_progress_report.report_type != "SR": if next_progress_report.report_type == "QPR": From 20862266192b546e014a3d143446cbabc0e3eeff Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 16:19:34 -0700 Subject: [PATCH 035/817] fix: More circular import fixes [#1094] --- django_api/django_api/apps/core/helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django_api/django_api/apps/core/helpers.py b/django_api/django_api/apps/core/helpers.py index 9614fe72d..c537d5f5f 100644 --- a/django_api/django_api/apps/core/helpers.py +++ b/django_api/django_api/apps/core/helpers.py @@ -14,8 +14,6 @@ PD_FREQUENCY_LEVEL, ) -from unicef.models import ProgressReport - logger = logging.getLogger("django") @@ -383,6 +381,7 @@ def create_pr_for_report_type(pd, idx, reporting_period, generate_from_date): Tuple[ProgressReport, datetime.datetime, datetime.datetime, datetime.datetime] - Newly generated ProgressReport & 3 datetime objects """ + from unicef.models import ProgressReport end_date = reporting_period.end_date due_date = reporting_period.due_date @@ -511,6 +510,8 @@ def create_ir_and_ilds_for_pr(pd, reportable_queryset, next_progress_report, sta Reportable, ) + from unicef.models import ProgressReport + if next_progress_report.report_type != "SR": if next_progress_report.report_type == "QPR": queryset = reportable_queryset From 84f7605fe0c008f801f8a637328b68e4f9466a20 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 17:11:18 -0700 Subject: [PATCH 036/817] fix: Use HasAnyRole function to impose permission on ReportRefreshAPIView [#1094] --- django_api/django_api/apps/indicator/views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/django_api/django_api/apps/indicator/views.py b/django_api/django_api/apps/indicator/views.py index 42f9418ee..054535caa 100644 --- a/django_api/django_api/apps/indicator/views.py +++ b/django_api/django_api/apps/indicator/views.py @@ -837,9 +837,14 @@ def post(self, request, *args, **kwargs): class ReportRefreshAPIView(APIView): permission_classes = ( - AnyPermission( - IsPartnerAuthorizedOfficerForCurrentWorkspace, - IsPartnerEditorForCurrentWorkspace), + IsAuthenticated, + HasAnyRole( + PRP_ROLE_TYPES.cluster_system_admin, + PRP_ROLE_TYPES.cluster_imo, + PRP_ROLE_TYPES.cluster_member, + PRP_ROLE_TYPES.ip_authorized_officer, + PRP_ROLE_TYPES.ip_editor, + ), ) @transaction.atomic From 7cda4930837fe123e0072142bcdc6d5842469861 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 17:19:12 -0700 Subject: [PATCH 037/817] fix: Fixed function call signatures in report generator and RefreshReportAPIView [#1094] + create_ir_and_ilds_for_pr + create_pr_for_report_type --- django_api/django_api/apps/core/tasks.py | 9 ++++++--- django_api/django_api/apps/indicator/utilities.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/django_api/django_api/apps/core/tasks.py b/django_api/django_api/apps/core/tasks.py index 1746b03db..314b0622a 100644 --- a/django_api/django_api/apps/core/tasks.py +++ b/django_api/django_api/apps/core/tasks.py @@ -244,10 +244,11 @@ def process_period_reports(): continue next_progress_report, start_date, end_date, due_date = create_pr_for_report_type( - reporting_period, generate_from_date_qpr + pd, idx, reporting_period, generate_from_date_qpr ) create_ir_and_ilds_for_pr( + pd, reportable_queryset, next_progress_report, start_date, @@ -275,10 +276,11 @@ def process_period_reports(): continue next_progress_report, start_date, end_date, due_date = create_pr_for_report_type( - reporting_period, generate_from_date_hr + pd, idx, reporting_period, generate_from_date_hr ) create_ir_and_ilds_for_pr( + pd, reportable_queryset, next_progress_report, start_date, @@ -299,10 +301,11 @@ def process_period_reports(): continue next_progress_report, start_date, end_date, due_date = create_pr_for_report_type( - reporting_period, generate_from_date_sr + pd, idx, reporting_period, generate_from_date_sr ) create_ir_and_ilds_for_pr( + pd, reportable_queryset, next_progress_report, start_date, diff --git a/django_api/django_api/apps/indicator/utilities.py b/django_api/django_api/apps/indicator/utilities.py index cfae7476c..5cc2a7443 100644 --- a/django_api/django_api/apps/indicator/utilities.py +++ b/django_api/django_api/apps/indicator/utilities.py @@ -73,6 +73,7 @@ def reset_progress_report_data(progress_report): with transaction.atomic(): create_ir_and_ilds_for_pr( + pd, reportable_queryset, progress_report, progress_report.start_date, From 01489983a4b8cdfe42f3ab2af432e318dcc00b3f Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 14 Mar 2019 17:23:49 -0700 Subject: [PATCH 038/817] fix: Fixed call signatures in helpers [#1094] + create_pr_ir_for_reportable --- django_api/django_api/apps/core/helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django_api/django_api/apps/core/helpers.py b/django_api/django_api/apps/core/helpers.py index c537d5f5f..0ae4cec24 100644 --- a/django_api/django_api/apps/core/helpers.py +++ b/django_api/django_api/apps/core/helpers.py @@ -527,6 +527,7 @@ def create_ir_and_ilds_for_pr(pd, reportable_queryset, next_progress_report, sta for reportable in queryset: indicator_report = create_pr_ir_for_reportable( + pd, reportable, None, start_date, @@ -575,6 +576,7 @@ def create_ir_and_ilds_for_pr(pd, reportable_queryset, next_progress_report, sta if pai_ir_for_period: indicator_report = create_pr_ir_for_reportable( + pd, reportable, pai_ir_for_period, pai_ir_for_period.time_period_start, From 4f81c0013056bdf9f32a1f218381c3efe818f0e0 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 15 Mar 2019 11:33:38 -0700 Subject: [PATCH 039/817] feat: Begin building delete functionality for attachment [#1088] --- polymer/src/elements/etools-prp-ajax.html | 1 + .../elements/ip-reporting/report-attachments.html | 12 +++++++++--- polymer/src/endpoints.html | 4 ++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/polymer/src/elements/etools-prp-ajax.html b/polymer/src/elements/etools-prp-ajax.html index 0f231aade..e57adc76f 100644 --- a/polymer/src/elements/etools-prp-ajax.html +++ b/polymer/src/elements/etools-prp-ajax.html @@ -124,6 +124,7 @@ }, _handleRequest: function () { + console.log(this.url) this.fire.apply(this, ['request'].concat(arguments)); }, diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 6131816f5..5a94aebad 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -26,13 +26,13 @@ + url="[[attachmentsListUrl]]"> + url="[[attachmentDeleteUrl]]"> Date: Fri, 15 Mar 2019 15:10:27 -0700 Subject: [PATCH 040/817] feat: Delete files working [#1088] --- polymer/src/elements/etools-prp-ajax.html | 1 - .../ip-reporting/report-attachments.html | 55 +++++++++---------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/polymer/src/elements/etools-prp-ajax.html b/polymer/src/elements/etools-prp-ajax.html index e57adc76f..0f231aade 100644 --- a/polymer/src/elements/etools-prp-ajax.html +++ b/polymer/src/elements/etools-prp-ajax.html @@ -124,7 +124,6 @@ }, _handleRequest: function () { - console.log(this.url) this.fire.apply(this, ['request'].concat(arguments)); }, diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 5a94aebad..aef0c02f1 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -36,26 +36,13 @@ - - - - - - @@ -75,17 +62,19 @@ }, properties: { - faceFile: Object, - otherOne: Object, - otherTwo: Object, readonly: Boolean, + fileTypes: { + type: Array, + computed: '_computeFileTypesArray()' + }, + pending: { type: Boolean, statePath: App.Selectors.ProgrammeDocumentReportsAttachments.pending, }, - attachments: { + attachments: { type: Array, statePath: App.Selectors.ProgrammeDocumentReportsAttachments.current, observer: '_setFiles', @@ -93,12 +82,11 @@ attachmentsListUrl: { type: String, - computed: '_computeAttachmentsUrl(locationId, reportId)', + computed: '_computeListUrl(locationId, reportId)', }, attachmentDeleteUrl: { - type: String, - computed: '_computeAttachmentsUrl(locationId, reportId)', + type: String }, locationId: { @@ -116,18 +104,28 @@ '_filesChanged(files.*)', ], - _computeAttachmentsUrl: function (locationId, reportId) { + _computeFileTypesArray: function () { + return ['FACE', 'Other']; + }, + + _computeListUrl: function (locationId, reportId) { return App.Endpoints.progressReportAttachments(locationId, reportId); }, + _getDeleteUrl: function (locationId, reportId, attachmentId) { + return App.Endpoints.progressReportAttachmentDetail(locationId, reportId, attachmentId); + }, + _onDeleteFile: function (e) { var self = this; var deleteThunk; + var deleteUrl = self._getDeleteUrl(self.locationId, self.reportId, e.detail.file.id); + + this.set('attachmentDeleteUrl', deleteUrl); e.stopPropagation(); deleteThunk = this.$.delete.thunk(); - console.log(deleteThunk); this.$.delete.abort(); @@ -136,6 +134,7 @@ ) .then(function () { self._notifyFileDeleted(); + this.set('attachmentDeleteUrl', undefined); }) .catch(function (err) { // jshint ignore:line // TODO: error handling @@ -143,9 +142,7 @@ }, _setFiles: function (attachments) { - console.log(attachments) attachments.forEach(attachment => { - if(attachment && !attachment.path) return; if(attachment.type === 'FACE') this.set('faceFile', [attachment].filter(Boolean)); From 72226be5a8afaad064f4a05eb9b9c77bd85bfad1 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 15 Mar 2019 15:53:40 -0700 Subject: [PATCH 041/817] feat: Render file types correctly and begin fixing upload [#1088] --- .../ip-reporting/report-attachments.html | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index aef0c02f1..db2392f3f 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -38,9 +38,10 @@ @@ -66,7 +67,30 @@ fileTypes: { type: Array, - computed: '_computeFileTypesArray()' + value: [ + { + "id": 3, + "name": "FACE" + }, + { + "id": 4, + "name": "Other" + } + ], + notify: true + }, + + fileModel: { + type: Object, + value: { + id: null, + file_name: null, + path: null, + size: null, + raw: null, + type: null + }, + notify: true }, pending: { @@ -77,7 +101,6 @@ attachments: { type: Array, statePath: App.Selectors.ProgrammeDocumentReportsAttachments.current, - observer: '_setFiles', }, attachmentsListUrl: { @@ -101,13 +124,9 @@ }, observers: [ - '_filesChanged(files.*)', + '_filesChanged(attachments.*)', ], - _computeFileTypesArray: function () { - return ['FACE', 'Other']; - }, - _computeListUrl: function (locationId, reportId) { return App.Endpoints.progressReportAttachments(locationId, reportId); }, @@ -141,19 +160,15 @@ }); }, - _setFiles: function (attachments) { - attachments.forEach(attachment => { - if(attachment && !attachment.path) return; - - if(attachment.type === 'FACE') this.set('faceFile', [attachment].filter(Boolean)); + _filesChanged: function (change) { + console.log('hellooooooo'); + console.log(change); - if(attachment.type === 'Other') { - this.otherOne.length === 0 ? this.set('otherOne', [attachment].filter(Boolean)) : this.set('otherTwo', [attachment].filter(Boolean)); - }; - }); - }, + // Loop through and find the file that null raw + // if path null and raw undefined, use that file and exit loop + // if replacing existing file, if path is defined and has a raw, that's the one to replace + // create file and pass down to debounce - _filesChanged: function (change) { this.debounce('files-changed', function () { var file = this.files[0]; var self = this; From 73b795cea9848d3084899f1accf9b501828cc0d0 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 15 Mar 2019 16:16:07 -0700 Subject: [PATCH 042/817] test: Created TestReportRefreshAPIView class [#1094] + setUp + test_invalid_serializer_values --- .../apps/indicator/tests/test_views.py | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/django_api/django_api/apps/indicator/tests/test_views.py b/django_api/django_api/apps/indicator/tests/test_views.py index f7bd793ef..16b7ed02c 100644 --- a/django_api/django_api/apps/indicator/tests/test_views.py +++ b/django_api/django_api/apps/indicator/tests/test_views.py @@ -1866,3 +1866,208 @@ def test_update_invalid_coordinate_space_value_format_validation(self): + "have correct value key structure: c, d, v", response.data['non_field_errors'][0] ) + + +class TestReportRefreshAPIView(BaseAPITestCase): + + def setUp(self): + self.country = CountryFactory() + self.workspace = WorkspaceFactory(countries=[self.country, ]) + self.response_plan = ResponsePlanFactory(workspace=self.workspace) + self.cluster = ClusterFactory(type='cccm', response_plan=self.response_plan) + self.loc_type = GatewayTypeFactory(country=self.country) + self.carto_table = CartoDBTableFactory(location_type=self.loc_type, country=self.country) + self.loc1 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.loc2 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.unicef_officer = PersonFactory() + self.unicef_focal_point = PersonFactory() + self.partner_focal_point = PersonFactory() + self.objective = ClusterObjectiveFactory( + cluster=self.cluster, + locations=[ + self.loc1, + self.loc2, + ] + ) + self.activity = ClusterActivityFactory( + cluster_objective=self.objective, + locations=[ + self.loc1, self.loc2 + ] + ) + self.partner = PartnerFactory(country_code=self.country.country_short_code) + self.user = NonPartnerUserFactory() + self.partner_user = PartnerUserFactory(partner=self.partner) + ClusterPRPRoleFactory(user=self.user, workspace=self.workspace, cluster=self.cluster, role=PRP_ROLE_TYPES.cluster_imo) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, role=PRP_ROLE_TYPES.ip_authorized_officer) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, cluster=None, role=PRP_ROLE_TYPES.cluster_member) + self.project = PartnerProjectFactory( + partner=self.partner, + clusters=[self.cluster], + locations=[self.loc1, self.loc2], + ) + self.p_activity = ClusterActivityPartnerActivityFactory( + cluster_activity=self.activity, + project=self.project, + ) + self.sample_disaggregation_value_map = { + "height": ["tall", "medium", "short", "extrashort"], + "age": ["1-2m", "3-4m", "5-6m", '7-10m', '11-13m', '14-16m'], + "gender": ["male", "female", "other"], + } + + blueprint = QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + calculation_formula_across_periods=IndicatorBlueprint.SUM, + ) + self.partneractivity_reportable = QuantityReportableToPartnerActivityFactory( + content_object=self.p_activity, blueprint=blueprint + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.partneractivity_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.partneractivity_reportable, + ) + + self.pd = ProgrammeDocumentFactory( + workspace=self.workspace, + partner=self.partner, + sections=[SectionFactory(), ], + unicef_officers=[self.unicef_officer, ], + unicef_focal_point=[self.unicef_focal_point, ], + partner_focal_point=[self.partner_focal_point, ] + ) + + for idx in range(2): + qpr_period = QPRReportingPeriodDatesFactory(programme_document=self.pd) + ProgressReportFactory( + start_date=qpr_period.start_date, + end_date=qpr_period.end_date, + due_date=qpr_period.due_date, + report_number=idx + 1, + report_type=qpr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + for idx in range(6): + hr_period = HRReportingPeriodDatesFactory(programme_document=self.pd) + ProgressReportFactory( + start_date=hr_period.start_date, + end_date=hr_period.end_date, + due_date=hr_period.due_date, + report_number=idx + 1, + report_type=hr_period.report_type, + is_final=False, + programme_document=self.pd, + submitted_by=self.user, + submitting_user=self.user, + ) + + self.cp_output = PDResultLinkFactory( + programme_document=self.pd, + ) + self.llo = LowerLevelOutputFactory( + cp_output=self.cp_output, + ) + self.llo_reportable = QuantityReportableToLowerLevelOutputFactory( + content_object=self.llo, + blueprint=QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + ) + ) + + self.llo_reportable.disaggregations.clear() + self.partneractivity_reportable.disaggregations.clear() + + # Create the disaggregations and values in the db for all response plans + # including one for no response plan as well + for disagg_name, values in self.sample_disaggregation_value_map.items(): + disagg = IPDisaggregationFactory(name=disagg_name) + cluster_disagg = DisaggregationFactory(name=disagg_name, response_plan=self.response_plan) + + self.llo_reportable.disaggregations.add(disagg) + self.partneractivity_reportable.disaggregations.add(cluster_disagg) + + for value in values: + DisaggregationValueFactory( + disaggregation=cluster_disagg, + value=value + ) + DisaggregationValueFactory( + disaggregation=disagg, + value=value + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.llo_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.llo_reportable, + ) + + for _ in range(2): + ClusterIndicatorReportFactory( + reportable=self.partneractivity_reportable, + report_status=INDICATOR_REPORT_STATUS.submitted, + ) + + # Creating Level-3 disaggregation location data for all locations + generate_3_num_disagg_data(self.partneractivity_reportable, indicator_type="quantity") + + for loc_data in IndicatorLocationData.objects.filter(indicator_report__reportable=self.partneractivity_reportable): + QuantityIndicatorDisaggregator.post_process(loc_data) + + for pr in self.pd.progress_reports.all(): + ProgressReportIndicatorReportFactory( + progress_report=pr, + reportable=self.llo_reportable, + report_status=INDICATOR_REPORT_STATUS.submitted, + overall_status=OVERALL_STATUS.met, + ) + + # Creating Level-3 disaggregation location data for all locations + generate_3_num_disagg_data(self.llo_reportable, indicator_type="quantity") + + for loc_data in IndicatorLocationData.objects.filter(indicator_report__reportable=self.llo_reportable): + QuantityIndicatorDisaggregator.post_process(loc_data) + + super().setUp() + + # Logging in as Partner AO + self.client.force_authenticate(self.partner_user) + + self.progress_report = self.pd.progress_reports.first() + + def test_invalid_serializer_values(self): + url = reverse('report-refresh-api') + data = {"report_type": "PR", "report_id": -100} + + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + data['report_id'] = self.progress_report.id + data['report_type'] = 'AAAA' + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + data['report_id'] = self.progress_report.indicator_reports.first().id + data['report_type'] = 'IR' + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + self.assertEquals( + "This indicator report is linked to a progress report. Use the progress report ID instead.", + response.data['non_field_errors'][0] + ) From 4cda8135766832845bfc39754626d93c6de8094e Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 15 Mar 2019 16:32:31 -0700 Subject: [PATCH 043/817] test: Added valid scenario test cases for TestReportRefreshAPIView [#1094] + test_progress_report_reset + test_cluster_indicator_report_reset --- .../apps/indicator/tests/test_views.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/django_api/django_api/apps/indicator/tests/test_views.py b/django_api/django_api/apps/indicator/tests/test_views.py index 16b7ed02c..84e3dacd6 100644 --- a/django_api/django_api/apps/indicator/tests/test_views.py +++ b/django_api/django_api/apps/indicator/tests/test_views.py @@ -2071,3 +2071,40 @@ def test_invalid_serializer_values(self): "This indicator report is linked to a progress report. Use the progress report ID instead.", response.data['non_field_errors'][0] ) + + def test_progress_report_reset(self): + url = reverse('report-refresh-api') + data = {"report_type": "PR", "report_id": self.progress_report.id} + + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_success(response.status_code)) + self.assertEquals(response.data['response'], "OK") + + # All indicator reports should be blank state + for ir in self.progress_report.indicator_reports.all(): + self.assertEquals(ir.total['c'], 0) + self.assertEquals(ir.overall_status, "NoS") + self.assertEquals(ir.report_status, "Due") + self.assertEquals(ir.submission_date, None) + + def test_cluster_indicator_report_reset(self): + ir = self.partneractivity_reportable.indicator_reports.first() + url = reverse('report-refresh-api') + data = {"report_type": "IR", "report_id": ir.id} + + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_success(response.status_code)) + self.assertEquals(response.data['response'], "OK") + + ir = IndicatorReport.objects.get(id=ir.id) + + # All indicator report information should be initial state + self.assertEquals(ir.submission_date, None) + self.assertEquals(ir.overall_status, "NoS") + self.assertEquals(ir.report_status, "Due") + self.assertEquals(ir.total['c'], 0) + + for ild in ir.indicator_location_data.all(): + self.assertEquals(ild.disaggregation['()']['c'], 0) + self.assertEquals(ild.disaggregation['()']['d'], 0) + self.assertEquals(ild.disaggregation['()']['v'], 0) From e149dd222d49ea706373e0e6befb2ca9bbb5aa1b Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 15 Mar 2019 16:45:00 -0700 Subject: [PATCH 044/817] docs: Added Python docstring for helper functions [#1094] --- django_api/django_api/apps/core/helpers.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/django_api/django_api/apps/core/helpers.py b/django_api/django_api/apps/core/helpers.py index 0ae4cec24..a9d5ab32a 100644 --- a/django_api/django_api/apps/core/helpers.py +++ b/django_api/django_api/apps/core/helpers.py @@ -417,6 +417,22 @@ def create_pr_for_report_type(pd, idx, reporting_period, generate_from_date): def create_pr_ir_for_reportable(pd, reportable, pai_ir_for_period, start_date, end_date, due_date): + """Create an IndicatorReport instance for ProgressReport instance later to attach. + If PartnerActivity IndicatorReport is present, the new IndicatorReport instance will associate + it as parent IndicatorReport instance, enabling dual-reporting feature. + + Arguments: + pd {ProgrammeDocument} -- ProgrammeDocument instance for logger + reportable {Reportable} -- Reportable instance to associate with new IndicatorReport instance + pai_ir_for_period {IndicatorReport} -- An optional IndicatorReport instance from PartnerActivity Reportable + start_date {datetime.date} -- Date object as report start date + end_date {datetime.date} -- Date object as report end date + due_date {datetime.date} -- Date object as report due date + + Returns: + IndicatorReport -- A newly created IndicatorReport instance + """ + from indicator.models import ( IndicatorBlueprint, IndicatorReport, @@ -497,6 +513,7 @@ def create_ir_and_ilds_for_pr(pd, reportable_queryset, next_progress_report, sta """ Create a set of new IndicatorReports and IndicatorLocationData instances per IndicatorReport instance, with passed-in new dates and new ProgressReport instance + Arguments: pd {ProgrammeDocument} -- ProgrammeDocument instnace reportable_queryset {django.Queryset[Reportable]} -- Reportable queryset on LLO From 9fb08491cbabd654ad512e860be18d9606493f7f Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 18 Mar 2019 10:36:37 -0700 Subject: [PATCH 045/817] feat: Continue work on upload and account for multiple attachments [#1088] --- .../ip-reporting/report-attachments.html | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index db2392f3f..ba5639427 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -69,11 +69,11 @@ type: Array, value: [ { - "id": 3, + "id": 0, "name": "FACE" }, { - "id": 4, + "id": 1, "name": "Other" } ], @@ -164,6 +164,14 @@ console.log('hellooooooo'); console.log(change); + var files = []; + + change.base.forEach(function(attachment) { + if(!attachment.path) { + files.push(attachment); + }; + }); + // Loop through and find the file that null raw // if path null and raw undefined, use that file and exit loop // if replacing existing file, if path is defined and has a raw, that's the one to replace @@ -175,7 +183,7 @@ var uploadThunk; var data; - if (change.path.split('.').length < 2 || !file) { + if (change.path.split('.').length < 2 || !files.length) { return; } @@ -185,8 +193,10 @@ data = new FormData(); - if (file) { - data.append('path', file.raw, file.file_name); + if (files.length) { + files.forEach(function(file) { + data.append('path', file.raw, file.file_name); + }) } this.$.upload.body = data; From c674302617c81db261973ed045f4e65327d5c4d0 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 18 Mar 2019 14:28:53 -0700 Subject: [PATCH 046/817] feat: Get attachment upload sort of functional [#1088] --- .../ip-reporting/report-attachments.html | 19 ++++++++----------- .../redux/actions/pdReportsAttachments.html | 2 ++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index ba5639427..9fc974b60 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -19,8 +19,8 @@ + method="post" + url="[[attachmentsListUrl]]"> Date: Mon, 18 Mar 2019 15:25:40 -0700 Subject: [PATCH 047/817] fix: indentation and cleanup console logs [#1088] --- .../elements/ip-reporting/report-attachments.html | 14 +++++++------- .../src/redux/actions/pdReportsAttachments.html | 2 -- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 9fc974b60..e83c09b92 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -200,13 +200,13 @@ this.dispatch( App.Actions.PDReportsAttachments.sync(uploadThunk, this.reportId) ) - .then(function () { - self._notifyFileUploaded(); - }) - .catch(function (err) { // jshint ignore:line - console.log(err); - // TODO: error handling - }); + .then(function () { + self._notifyFileUploaded(); + }) + .catch(function (err) { // jshint ignore:line + console.log(err); + // TODO: error handling + }); }); }, diff --git a/polymer/src/redux/actions/pdReportsAttachments.html b/polymer/src/redux/actions/pdReportsAttachments.html index 507ef995a..31ddd9484 100644 --- a/polymer/src/redux/actions/pdReportsAttachments.html +++ b/polymer/src/redux/actions/pdReportsAttachments.html @@ -15,12 +15,10 @@ return attachmentsThunk() .then(function (res) { - console.log('RESPONSE', res); dispatch(App.Actions.PDReportsAttachments.loadingStop(reportId)); dispatch(App.Actions.PDReportsAttachments.set(reportId, res.data)); }) .catch(function (err) { - console.log('ERRORRRRR', err) dispatch(App.Actions.PDReportsAttachments.loadingStop(reportId)); return Promise.reject(err); From 24500387565741cb05d11b248e4fe62f07328c0a Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 18 Mar 2019 15:27:42 -0700 Subject: [PATCH 048/817] chore: Clean up unnecessary comments [#1088] --- polymer/src/elements/ip-reporting/report-attachments.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index e83c09b92..74d42e471 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -170,11 +170,6 @@ }; }); - // Loop through and find the file that null raw - // if path null and raw undefined, use that file and exit loop - // if replacing existing file, if path is defined and has a raw, that's the one to replace - // create file and pass down to debounce - this.debounce('files-changed', function () { var self = this; var uploadThunk; From 8ef614a24aa75eee30950634ca0ddd37dac4fb51 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 18 Mar 2019 15:27:54 -0700 Subject: [PATCH 049/817] feat: Added `Refresh` button on Progress Report header UI [#1094] + etools-prp-ajax + template DOM for paper-button + refreshData, canRefresh, refreshUrl, _computeRefreshData, _computeCanRefresh, _refresh properties and functions + Endpoints to add reportProgressReset --- polymer/src/endpoints.html | 4 + .../pages/app/ip-reporting/pd/pd-report.html | 77 ++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/polymer/src/endpoints.html b/polymer/src/endpoints.html index ac6c678a0..24d13a3c4 100644 --- a/polymer/src/endpoints.html +++ b/polymer/src/endpoints.html @@ -73,6 +73,10 @@ return this.programmeDocumentReportSubmit(workspaceId, reportId) + 'sr/'; }, + reportProgressReset: function () { + return this._buildUrl('/indicator/report-refresh/'); + }, + programmeDocumentReportUpdate: function (workspaceId, reportId) { return this.programmeDocumentReport(workspaceId, reportId) + 'update/'; }, diff --git a/polymer/src/pages/app/ip-reporting/pd/pd-report.html b/polymer/src/pages/app/ip-reporting/pd/pd-report.html index 69e5aaf49..994210ed5 100644 --- a/polymer/src/pages/app/ip-reporting/pd/pd-report.html +++ b/polymer/src/pages/app/ip-reporting/pd/pd-report.html @@ -106,6 +106,14 @@ method="post"> + + + @@ -116,7 +124,7 @@ + report="[[currentReport]]"> + +
+ diff --git a/polymer/src/elements/refresh-report-modal.html b/polymer/src/elements/refresh-report-modal.html index d3295b423..4a5c850f0 100644 --- a/polymer/src/elements/refresh-report-modal.html +++ b/polymer/src/elements/refresh-report-modal.html @@ -68,7 +68,7 @@

is="dom-if" if="[[_equals(data.report_type, 'IR')]]" restamp="true"> - You are about to delete all location data for this Indicator Report. New location data entries will be generated. + You are about to reset all location data and entered info for this Indicator Report; including Overall status, Narrative Assessment, and Report Status. New location data entries will be generated. This action is irreversible. Please click "Refresh" if you wish to proceed.

From b5735eafb52741b1e6a33903bfd9589fff578299 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 18 Mar 2019 16:46:58 -0700 Subject: [PATCH 052/817] docs: Gitbook updates [#1094] + Reporting on results + Reporting process --- .../assets/cluster_indicator_refresh.png | Bin 0 -> 29275 bytes .../assets/cluster_indicator_refresh_modal.png | Bin 0 -> 104164 bytes .../assets/progress_report_refresh.png | Bin 0 -> 17879 bytes .../assets/progress_report_refresh_modal.png | Bin 0 -> 102617 bytes .../cluster-reporting/reporting-on-results.md | 9 +++++++++ .../progress-reports/reporting-process.md | 9 +++++++++ 6 files changed, 18 insertions(+) create mode 100644 docs/.gitbook/assets/cluster_indicator_refresh.png create mode 100644 docs/.gitbook/assets/cluster_indicator_refresh_modal.png create mode 100644 docs/.gitbook/assets/progress_report_refresh.png create mode 100644 docs/.gitbook/assets/progress_report_refresh_modal.png diff --git a/docs/.gitbook/assets/cluster_indicator_refresh.png b/docs/.gitbook/assets/cluster_indicator_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..b56af286a2a6ed9a5338510a56e4d40e0472e87d GIT binary patch literal 29275 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 EKj?!9Gynhq literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/cluster_indicator_refresh_modal.png b/docs/.gitbook/assets/cluster_indicator_refresh_modal.png new file mode 100644 index 0000000000000000000000000000000000000000..d400bc990d48cee0a743964dc9d6accf19b8c658 GIT binary patch literal 104164 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�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 literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/progress_report_refresh.png b/docs/.gitbook/assets/progress_report_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..2e97928fabae5c75fc167579f020eee4b66cff42 GIT binary patch literal 17879 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 BU@HIs literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/progress_report_refresh_modal.png b/docs/.gitbook/assets/progress_report_refresh_modal.png new file mode 100644 index 0000000000000000000000000000000000000000..cf3fa397ae204443d14dd3271eeacd40170dbfb2 GIT binary patch literal 102617 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

WdORY{0{cAeTVz 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
>ApP-!r>td*M58#8uVG5JE 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 g{J+f@BfI Date: Tue, 19 Mar 2019 13:04:08 -0700 Subject: [PATCH 053/817] refactor: Extracted Refresh button from header to pd-output-list-toolbar component [#1094] --- .../ip-reporting/pd-output-list-toolbar.html | 82 ++++++++++++++++++- .../pages/app/ip-reporting/pd/pd-report.html | 63 -------------- 2 files changed, 80 insertions(+), 65 deletions(-) diff --git a/polymer/src/elements/ip-reporting/pd-output-list-toolbar.html b/polymer/src/elements/ip-reporting/pd-output-list-toolbar.html index 63b702503..a41e23720 100644 --- a/polymer/src/elements/ip-reporting/pd-output-list-toolbar.html +++ b/polymer/src/elements/ip-reporting/pd-output-list-toolbar.html @@ -1,19 +1,37 @@ + + + + @@ -42,7 +72,7 @@ behaviors: [ App.Behaviors.UtilsBehavior, - App.Behaviors.ReduxBehavior + App.Behaviors.ReduxBehavior, ], properties: { @@ -80,6 +110,29 @@ type: String, computed: '_appendQuery(pdReportUrl, query, \'export=xlsx\')', }, + + currentReport: { + type: Object, + statePath: App.Selectors.ProgrammeDocumentReports.current, + }, + + refreshData: { + type: Object, + computed: '_computeRefreshData(reportId)', + }, + + canRefresh: { + type: Boolean, + value: false, + computed: '_computeCanRefresh(currentReport, programmeDocument)', + }, + + refreshUrl: { + type: String, + value: function () { + return App.Endpoints.reportProgressReset(); + }, + }, }, listeners: { @@ -102,11 +155,36 @@ return App.Endpoints.programmeDocumentReport(locationId, reportId); }, + _computeRefreshData: function (reportId) { + return {"report_id": reportId, "report_type": "PR"}; + }, + + _computeCanRefresh: function (report, programmeDocument) { + switch (true) { + case programmeDocument && + (programmeDocument.status === 'Sig' + || programmeDocument.status === 'Clo'): + case programmeDocument && report.report_type === 'SR': + return false; + + default: + return true; + } + }, + + _refresh: function () { + this.$.refresh.open(); + }, + _onFileUploaded: function (e) { e.stopPropagation(); window.location.reload(); }, + + detached: function () { + this.$.refresh.close(); + }, }); diff --git a/polymer/src/pages/app/ip-reporting/pd/pd-report.html b/polymer/src/pages/app/ip-reporting/pd/pd-report.html index d5489e785..d4750a2af 100644 --- a/polymer/src/pages/app/ip-reporting/pd/pd-report.html +++ b/polymer/src/pages/app/ip-reporting/pd/pd-report.html @@ -27,7 +27,6 @@ - @@ -107,14 +106,6 @@ method="post"> - - - @@ -154,19 +145,6 @@ - -

- diff --git a/polymer/src/elements/reportable-meta.html b/polymer/src/elements/reportable-meta.html index e68f7989b..c04f631ed 100644 --- a/polymer/src/elements/reportable-meta.html +++ b/polymer/src/elements/reportable-meta.html @@ -7,11 +7,13 @@ + + \ No newline at end of file diff --git a/polymer/src/behaviors/single-selection-menu-missing-options-behavior.html b/polymer/src/behaviors/single-selection-menu-missing-options-behavior.html new file mode 100644 index 000000000..a17e652c2 --- /dev/null +++ b/polymer/src/behaviors/single-selection-menu-missing-options-behavior.html @@ -0,0 +1,109 @@ + \ No newline at end of file diff --git a/polymer/src/behaviors/single-selection-menu-split-label-behavior.html b/polymer/src/behaviors/single-selection-menu-split-label-behavior.html new file mode 100644 index 000000000..f90506681 --- /dev/null +++ b/polymer/src/behaviors/single-selection-menu-split-label-behavior.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/polymer/src/elements/cluster-reporting/es6-polyfills.html b/polymer/src/elements/cluster-reporting/es6-polyfills.html new file mode 100644 index 000000000..57f60b943 --- /dev/null +++ b/polymer/src/elements/cluster-reporting/es6-polyfills.html @@ -0,0 +1,98 @@ + \ No newline at end of file diff --git a/polymer/src/elements/cluster-reporting/etools-prp-single-selection-menu-dropdown-shared-styles.html b/polymer/src/elements/cluster-reporting/etools-prp-single-selection-menu-dropdown-shared-styles.html new file mode 100644 index 000000000..5c81e2e2c --- /dev/null +++ b/polymer/src/elements/cluster-reporting/etools-prp-single-selection-menu-dropdown-shared-styles.html @@ -0,0 +1,179 @@ + + + + \ No newline at end of file diff --git a/polymer/src/elements/cluster-reporting/etools-prp-single-selection-menu.html b/polymer/src/elements/cluster-reporting/etools-prp-single-selection-menu.html new file mode 100644 index 000000000..6aa0754de --- /dev/null +++ b/polymer/src/elements/cluster-reporting/etools-prp-single-selection-menu.html @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 423ff55e3..73761cd25 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -4,9 +4,10 @@ - + + @@ -218,6 +219,7 @@

Locations ([[value.length]])

option-value="id" option-label="title" selected="{{item.location}}" + search="{{search}}" data-index$="[[index]]" disabled="[[_getPending(pending, item.loc_type)]]" required> @@ -288,6 +290,12 @@

Locations ([[value.length]])

locations: Object, pending: Object, isPai: Boolean, + searchLocationType: String, + + search: { + type: String, + observer: '_getNewSearch', + }, editing: { type: Boolean, @@ -357,6 +365,34 @@

Locations ([[value.length]])

}, }, + _getNewSearch: function (value, oldValue) { + console.log('VALUE', value); + if(value === '' || value === oldValue) { + console.log('HELLO'); + return; + } + + var loc_type = this.get('searchLocationType'); + + this.debounce('fetch-locations-' + loc_type, function () { + var self = this; + + this._setPending(loc_type, true); + + getThunk = this.$$('#locations' + loc_type).thunk()(); + console.log('GETTHUNK', getThunk); + + // this.$$('#locations' + loc_type).thunk()() + // .then(function (res) { + // self._setPending(loc_type, false); + // self._setLocations(loc_type, res.data.results); + // }) + // .catch(function () { + // self._setPending(loc_type, false); + // }); + }, 100); + }, + _computeIsNumber: function (type) { return type === 'number'; }, @@ -463,6 +499,7 @@

Locations ([[value.length]])

_fetchInitialLocations: function (lockedItems) { lockedItems.forEach(function (item) { this._fetchLocations(item.loc_type); + this.set('searchLocationType', item.loc_type); }, this); }, From c7bbe56bd7d0421cbf35e0efe196ae7180a13d20 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 2 Apr 2019 09:03:21 -0700 Subject: [PATCH 097/817] feat: Get etools-selection-menu to fire on search change [#1111] --- .../cluster-reporting/indicator-locations-widget.html | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 73761cd25..d8d51d9e3 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -219,7 +219,7 @@

Locations ([[value.length]])

option-value="id" option-label="title" selected="{{item.location}}" - search="{{search}}" + on-value-changed="_getNewSearch" data-index$="[[index]]" disabled="[[_getPending(pending, item.loc_type)]]" required> @@ -292,11 +292,6 @@

Locations ([[value.length]])

isPai: Boolean, searchLocationType: String, - search: { - type: String, - observer: '_getNewSearch', - }, - editing: { type: Boolean, value: false, @@ -366,7 +361,7 @@

Locations ([[value.length]])

}, _getNewSearch: function (value, oldValue) { - console.log('VALUE', value); + console.log('VALUE', event.detail.value); if(value === '' || value === oldValue) { console.log('HELLO'); return; From 05fd84f4f90471c449867ef88f6765210521c91b Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 2 Apr 2019 13:18:48 -0700 Subject: [PATCH 098/817] feat: Begin building out method for iterating through pages [#1111] --- .../indicator-locations-widget.html | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index d8d51d9e3..38861f8df 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -109,6 +109,10 @@ + + + + + + \ No newline at end of file diff --git a/polymer/src/elements/ip-reporting/js/pd-reports-list.js b/polymer/src/elements/ip-reporting/js/pd-reports-list.js new file mode 100644 index 000000000..bb6129de8 --- /dev/null +++ b/polymer/src/elements/ip-reporting/js/pd-reports-list.js @@ -0,0 +1,14 @@ +function PdReportsListUtils() { + +} + +PdReportsListUtils.getLink = function (report, suffix, buildUrlFn, baseUrl) { + return buildUrlFn( + baseUrl, + '/pd/' + report.programme_document.id + '/report/' + report.id + '/' + suffix + ); +} + +try { + module.exports = exports = PdReportsListUtils; +} catch (e) {} \ No newline at end of file diff --git a/polymer/src/elements/ip-reporting/pd-reports-list.html b/polymer/src/elements/ip-reporting/pd-reports-list.html index 0d6670c2c..2dbe7775e 100644 --- a/polymer/src/elements/ip-reporting/pd-reports-list.html +++ b/polymer/src/elements/ip-reporting/pd-reports-list.html @@ -19,6 +19,7 @@ + +
@@ -294,19 +302,33 @@

[[_computeTitle(edit)]]

is="dom-if" if="[[_equals(mode, 'ocha')]]" restamp="true"> -
- - +
+
+ + +
+ +
+ + +
- - - -
+ +
@@ -669,17 +691,11 @@

Fields required

- -
+ -
- - - - - - + + diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index d226f0c6b..e55bface9 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -875,11 +875,6 @@

Fields required

value: '' }, - selectedIndicatorUrl: { - type: String, - computed: '_computeSelectedIndicatorUrl(responsePlanID, selectedIndicator)' - }, - indicatorsUrl: { type: String, computed: '_computeIndicatorsUrl(responsePlanID)', @@ -1017,7 +1012,7 @@

Fields required

'_updateMode(fromOchaDisabled, customDisabled)', '_fetchObjectivesList(selectedCluster)', '_fetchIndicatorsList(selectedObjective)', - '_fetchSelectedIndicatorDetailType(selectedIndicatorUrl)', + '_fetchSelectedIndicatorDetailType(responsePlanID, selectedIndicator)', ], _computeFromOchaDisabled: function (partner, selectedPartner) { @@ -1047,10 +1042,6 @@

Fields required

} }, - _computeSelectedIndicatorUrl: function(responsePlanID, selectedIndicatorId) { - return App.Endpoints.analysisIndicator(responsePlanID, selectedIndicatorId); - }, - _computeOchaProjectsUrl: function (responsePlanID) { return App.Endpoints.ochaProjectsList(responsePlanID); }, @@ -1260,8 +1251,8 @@

Fields required

}, 100); }, - _fetchSelectedIndicatorDetailType: function (selectedIndicatorUrl) { - if (selectedIndicatorUrl === undefined) { + _fetchSelectedIndicatorDetailType: function (responsePlanID, selectedIndicator) { + if (selectedIndicator === undefined) { return; } @@ -1269,6 +1260,9 @@

Fields required

var self = this; this.set('selectedIndicatorDetailType', ''); + this.set('data.reportable_id', selectedIndicator); + + this.$.indicatorDetail.url = App.Endpoints.analysisIndicator(responsePlanID, selectedIndicator); this.$.indicatorDetail.abort(); this.$.indicatorDetail.thunk()() @@ -1315,11 +1309,11 @@

Fields required

this.set('projectLoading', false); this.set('selectedPartner', {}); this.set('data', { - partner_id: '', - partner_project_id: '', - cluster_id: '', - cluster_objective_id: '', - reportable_id: '', + partner_id: null, + partner_project_id: null, + cluster_id: null, + cluster_objective_id: null, + reportable_id: null, locations: [], target: {}, baseline: {} @@ -1331,13 +1325,6 @@

Fields required

_save: function () { var self = this; this.data.partner_project_id = 5; - this.data.reportable_id = 2000; - this.data.baseline.d = 1; - this.data.target.d = 1; - this.data.baseline.v = parseInt(this.data.baseline.v); - this.data.target.v = parseInt(this.data.target.v); - this.data.baseline.c = this.data.baseline.v / this.data.baseline.d; - this.data.target.c = this.data.target.v / this.data.target.d; if (!this.data.partner_id) { this.data.partner_id = this.partnerID; @@ -1351,7 +1338,6 @@

Fields required

console.log('body', this.data); this.$.indicatorAjax.body = this.data; - // return; var thunk = this.$.indicatorAjax.thunk(); thunk() diff --git a/polymer/src/endpoints.html b/polymer/src/endpoints.html index f941dbf60..0d6f16b50 100644 --- a/polymer/src/endpoints.html +++ b/polymer/src/endpoints.html @@ -242,6 +242,10 @@ return this._buildUrl('/indicator/cluster-indicator/'); }, + adoptedClusterIndicators: function () { + return this._buildUrl('/indicator/partner-project-indicator-adopt/') + }, + partnerActivity: function (responsePlanId, mode) { return this._buildUrl('/partner/' + responsePlanId + '/create-partner-activity/' + mode); }, From c8cdb2293212ed85454fa4c0b193b309002ccb0b Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 1 May 2019 17:51:48 -0700 Subject: [PATCH 289/817] fix: Tweak conditional check and verify custom indicator still works [ch10756] --- .../cluster-reporting/indicator-modal.html | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 620e76ca9..e963149f2 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -728,17 +728,6 @@

[[modalTitle]]

return App.Endpoints.responseParametersClusterDisaggregations(responsePlanId); }, - _computeIndicatorsUrl: function (mode) { - if (mode === 'objectives') { - console.log('HERE') - return App.Endpoints.adoptedClusterIndicators(); - } else if (mode === 'custom') { - return App.Endpoints.clusterIndicators(); - } else { - return; - } - }, - _computeIndicatorsListUrl: function (responsePlanId) { return App.Endpoints.clusterIndicatorNames(responsePlanId); }, @@ -751,10 +740,6 @@

[[modalTitle]]

return App.Endpoints.responseParametersClusterObjectives(responsePlanId); }, - _computeIndicatorsUrl: function (responsePlanId) { - return App.Endpoints.clusterIndicatorNames(responsePlanId); - }, - _fetchDisaggregations: function() { var self = this; @@ -881,7 +866,7 @@

[[modalTitle]]

return; } - if(this.mode = 'objectives') { + if(this.mode === 'objectives') { console.log('objectives saved'); console.log('objectives url', this.$.indicators.url); console.log('data', this.data); @@ -897,16 +882,11 @@

[[modalTitle]]

self.set('errors', err.data); self.set('updatePending', false); }); - - return; } else { this.set('updatePending', true); this._setDisaggregations(); this.$.indicators.body = this._processData(this.data); - console.log(this.$.indicators.body); - return; - this.$.indicators.thunk()() .then(function (res) { self.fire('indicator-added', res.data); From c17b5de06f8383e6aaf45a60ca06f2dfe8965294 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 1 May 2019 17:54:12 -0700 Subject: [PATCH 290/817] fix: Restore creation-modal for Add Project is back to the way it was [ch10756] --- .../projects/creation-modal.html | 381 ++++-------------- 1 file changed, 77 insertions(+), 304 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index e55bface9..b511b7ab4 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -41,7 +41,6 @@ - @@ -206,11 +205,11 @@ + method="[[_computeAjaxMethod(edit)]]"> - - - - - - - - - [[_computeTitle(edit)]]

- [[_computeTitle(edit)]] restamp="true"> - Adopt from Cluster Objective + name="ocha"> + From OCHA -
+ empty$="[[!_equals(mode, 'ocha')]]">
- -
@@ -753,11 +669,17 @@

Fields required

+ +
-
- - +
+ + + + + + - + \ No newline at end of file From 89c64d7fcd63e9791788974a75cfd45fbf07b439 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 2 May 2019 13:37:15 -0700 Subject: [PATCH 291/817] feat: Correctly fetch objective indicators instead of all indicators [ch10756] --- .../cluster-reporting/indicator-modal.html | 64 ++++++++++++++----- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index e963149f2..7dabf9bf7 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -101,13 +101,14 @@ id="objectives" timeout="100000" url="[[objectivesUrl]]" - params=[[objectivesParams]]> + params="[[objectivesParams]]"> + url="[[indicatorsListUrl]]" + params="[[indicatorsListParams]]"> [[modalTitle]] indicatorsListUrl: { type: String, - computed: '_computeIndicatorsListUrl(responsePlanId)', + value: App.Endpoints.indicators('co') + '/', + }, + + indicatorsListParams: { + type: Object, + value: {}, }, clusters: { @@ -685,14 +691,14 @@

[[modalTitle]]

return; } else if (this.mode === 'objectives') { this.set('data', { - partner_id: null, - partner_project_id: null, - cluster_id: null, - cluster_objective_id: null, - reportable_id: null, + partner_id: '', + partner_project_id: '', + cluster_id: '', + cluster_objective_id: '', + reportable_id: '', locations: [], - target: {}, - baseline: {} + target: {d: 1}, + baseline: {d: 1} }); this.set('indicatorsUrl', App.Endpoints.adoptedClusterIndicators()); @@ -728,10 +734,6 @@

[[modalTitle]]

return App.Endpoints.responseParametersClusterDisaggregations(responsePlanId); }, - _computeIndicatorsListUrl: function (responsePlanId) { - return App.Endpoints.clusterIndicatorNames(responsePlanId); - }, - _computeMinDate: function (date) { return date ? this._normalizeDate(date) : null; }, @@ -783,7 +785,9 @@

[[modalTitle]]

return; } this.set('data.cluster_objective_id', selectedObjectiveId); - console.log('data from fetchObjectives', this.data); + this.set('indicatorsListParams', {object_id: selectedObjectiveId}); + + console.log('url', this.indicatorsListUrl); this.debounce('fetch-indicators', function () { var self = this; @@ -793,7 +797,17 @@

[[modalTitle]]

this.$.indicatorsList.abort(); this.$.indicatorsList.thunk()() .then(function (res) { - self.set('indicators', res.data); + var simpleIndicatorsList = []; + + res.data.results.forEach(function (indicator) { + var simpleIndicator = {}; + simpleIndicator.id = indicator.id; + simpleIndicator.title = indicator.blueprint.title; + + simpleIndicatorsList.push(simpleIndicator); + }); + + self.set('indicators', simpleIndicatorsList); self.fire('details-loaded'); }) .catch(function (err) { // jshint ignore:line @@ -867,9 +881,23 @@

[[modalTitle]]

} if(this.mode === 'objectives') { + this._setDisaggregations(); + + this.data.baseline.v = parseInt(this.data.baseline.v); + this.data.target.v = parseInt(this.data.target.v); + this.data.baseline.c = this.data.baseline.v / this.data.baseline.d; + this.data.target.c = this.data.target.v / this.data.target.d; + + this.data.locations.forEach(function (location) { + location.id = location.loc_type; + delete location.loc_type; + }); + + this.$.indicators.body = this._processData(this.data); + console.log('objectives saved'); console.log('objectives url', this.$.indicators.url); - console.log('data', this.data); + console.log('adopt data', this.data); this.$.indicators.thunk()() .then(function (res) { @@ -887,6 +915,8 @@

[[modalTitle]]

this._setDisaggregations(); this.$.indicators.body = this._processData(this.data); + console.log('custom data', this.$.indicators.body); + this.$.indicators.thunk()() .then(function (res) { self.fire('indicator-added', res.data); From 6a1a872711756d2d892ec2a17a4709846ef18ad5 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 2 May 2019 14:00:21 -0700 Subject: [PATCH 292/817] feat: Properly grab project ID to post indicator to correct project [ch10756] --- polymer/src/elements/cluster-reporting/indicator-modal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 7dabf9bf7..055cdc3db 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -758,7 +758,7 @@

[[modalTitle]]

this.set('data.partner_id', this.partnerID); this.set('data.cluster_id', selectedClusterId); - this.set('data.partner_project_id', 5); // REMOVE AND FIX + this.set('data.partner_project_id', this.objectId); console.log('data from fetchObjectives', this.data); From dd64887039590671c2d67c17615c8201539579ca Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 2 May 2019 14:09:06 -0700 Subject: [PATCH 293/817] feat: Added additional validations on ClusterObjectiveIndicatorAdoptSerializer [ch10756] + PartnerProject membership check to Partner + ClusterObjective membership check to Cluster --- django_api/django_api/apps/indicator/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/django_api/django_api/apps/indicator/serializers.py b/django_api/django_api/apps/indicator/serializers.py index 293135cd6..d14bc196e 100644 --- a/django_api/django_api/apps/indicator/serializers.py +++ b/django_api/django_api/apps/indicator/serializers.py @@ -1144,6 +1144,11 @@ def validate(self, data): 'partner_project_id': 'PartnerProject does not exist' }) + if not PartnerProject.objects.get(id=data['partner_project_id']).partner.id != data['partner_id']: + raise serializers.ValidationError({ + 'partner_project_id': 'This partner project does not belong to the partner' + }) + if not Cluster.objects.filter(id=data['cluster_id']).exists(): raise serializers.ValidationError({ 'cluster_id': 'Cluster does not exist' @@ -1154,6 +1159,11 @@ def validate(self, data): 'cluster_objective_id': 'ClusterObjective does not exist' }) + if not ClusterObjective.objects.get(id=data['cluster_objective_id']).cluster.id != data['cluster_id']: + raise serializers.ValidationError({ + 'cluster_objective_id': 'This objective does not belong to the cluster' + }) + reportables = Reportable.objects.filter(id=data['reportable_id']) if not reportables.exists(): From 177cf966c76d69250ef7cfc448c78681947c6fd5 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 2 May 2019 14:10:10 -0700 Subject: [PATCH 294/817] feat: Clean up code and diagnose why new indicator does not have correct values [ch10756] --- polymer/src/elements/cluster-reporting/indicator-modal.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 055cdc3db..06c57df26 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -757,11 +757,8 @@

[[modalTitle]]

} this.set('data.partner_id', this.partnerID); this.set('data.cluster_id', selectedClusterId); - this.set('data.partner_project_id', this.objectId); - console.log('data from fetchObjectives', this.data); - this.debounce('fetch-objectives', function () { var self = this; @@ -787,8 +784,6 @@

[[modalTitle]]

this.set('data.cluster_objective_id', selectedObjectiveId); this.set('indicatorsListParams', {object_id: selectedObjectiveId}); - console.log('url', this.indicatorsListUrl); - this.debounce('fetch-indicators', function () { var self = this; From d61db4c23d2f6f7cf5b1da3b7d96201c43d8c6c3 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 2 May 2019 14:53:06 -0700 Subject: [PATCH 295/817] test: Implemented test case for serializer data validation in TestClusterObjectiveIndicatorAdoptAPIViewAPIView [ch10756] + Target value type check + Baseline value type check + zero d value in target + missing v value in target + zero d value in baseline + missing v value in baseline + Non-exist partner check + Non-exist partner project check + Invalid partner project membership check + Non-exist cluster check + Non-exist cluster objective check + Invalid cluster objective membership check + Non-exist reportable_id check + Invalid reportable type check + Invalid reportable membership check --- .../apps/indicator/tests/test_views.py | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) diff --git a/django_api/django_api/apps/indicator/tests/test_views.py b/django_api/django_api/apps/indicator/tests/test_views.py index 177fa916b..2b1b59f22 100644 --- a/django_api/django_api/apps/indicator/tests/test_views.py +++ b/django_api/django_api/apps/indicator/tests/test_views.py @@ -33,6 +33,7 @@ PartnerUserFactory, PartnerFactory, ProgressReportFactory, QuantityReportableToLowerLevelOutputFactory, + QuantityReportableToClusterObjectiveFactory, QuantityTypeIndicatorBlueprintFactory, WorkspaceFactory, SectionFactory, @@ -2117,3 +2118,256 @@ def test_cluster_indicator_report_reset(self): self.assertEquals(ild.disaggregation['()']['c'], 0) self.assertEquals(ild.disaggregation['()']['d'], 0) self.assertEquals(ild.disaggregation['()']['v'], 0) + + +class TestClusterObjectiveIndicatorAdoptAPIViewAPIView(BaseAPITestCase): + + def setUp(self): + self.country = CountryFactory() + self.workspace = WorkspaceFactory(countries=[self.country, ]) + self.response_plan = ResponsePlanFactory(workspace=self.workspace) + self.cluster = ClusterFactory(type='cccm', response_plan=self.response_plan) + self.cluster2 = ClusterFactory(type='education', response_plan=self.response_plan) + self.loc_type = GatewayTypeFactory(country=self.country) + self.carto_table = CartoDBTableFactory(location_type=self.loc_type, country=self.country) + self.loc1 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.loc2 = LocationFactory(gateway=self.loc_type, carto_db_table=self.carto_table) + self.unicef_officer = PersonFactory() + self.unicef_focal_point = PersonFactory() + self.partner_focal_point = PersonFactory() + self.objective = ClusterObjectiveFactory( + cluster=self.cluster, + locations=[ + self.loc1, + self.loc2, + ] + ) + self.objective2 = ClusterObjectiveFactory( + cluster=self.cluster2, + locations=[ + self.loc1, + self.loc2, + ] + ) + self.activity = ClusterActivityFactory( + cluster_objective=self.objective, + locations=[ + self.loc1, self.loc2 + ] + ) + self.partner = PartnerFactory(country_code=self.country.country_short_code) + self.partner2 = PartnerFactory(country_code=self.country.country_short_code) + self.user = NonPartnerUserFactory() + self.partner_user = PartnerUserFactory(partner=self.partner) + ClusterPRPRoleFactory(user=self.user, workspace=self.workspace, cluster=self.cluster, role=PRP_ROLE_TYPES.cluster_imo) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, role=PRP_ROLE_TYPES.ip_authorized_officer) + IPPRPRoleFactory(user=self.partner_user, workspace=self.workspace, cluster=None, role=PRP_ROLE_TYPES.cluster_member) + self.project = PartnerProjectFactory( + partner=self.partner, + clusters=[self.cluster], + locations=[self.loc1, self.loc2], + ) + self.project2 = PartnerProjectFactory( + partner=self.partner2, + clusters=[self.cluster], + locations=[self.loc1, self.loc2], + ) + self.p_activity = ClusterActivityPartnerActivityFactory( + cluster_activity=self.activity, + project=self.project, + ) + self.sample_disaggregation_value_map = { + "height": ["tall", "medium", "short", "extrashort"], + "age": ["1-2m", "3-4m", "5-6m", '7-10m', '11-13m', '14-16m'], + "gender": ["male", "female", "other"], + } + + blueprint = QuantityTypeIndicatorBlueprintFactory( + unit=IndicatorBlueprint.NUMBER, + calculation_formula_across_locations=IndicatorBlueprint.SUM, + calculation_formula_across_periods=IndicatorBlueprint.SUM, + ) + + self.partneractivity_reportable = QuantityReportableToPartnerActivityFactory( + content_object=self.p_activity, blueprint=blueprint + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.partneractivity_reportable, + ) + + LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.partneractivity_reportable, + ) + + self.clusterobjective_reportable = QuantityReportableToClusterObjectiveFactory( + content_object=self.objective, blueprint=blueprint + ) + + self.reportable_loc_1 = LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.clusterobjective_reportable, + ) + + self.reportable_loc_2 = LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.clusterobjective_reportable, + ) + + self.clusterobjective_reportable2 = QuantityReportableToClusterObjectiveFactory( + content_object=self.objective2, blueprint=blueprint + ) + + self.reportable_2_loc_1 = LocationWithReportableLocationGoalFactory( + location=self.loc1, + reportable=self.clusterobjective_reportable2, + ) + + self.reportable_2_loc_2 = LocationWithReportableLocationGoalFactory( + location=self.loc2, + reportable=self.clusterobjective_reportable2, + ) + + self.clusterobjective_reportable.disaggregations.clear() + self.clusterobjective_reportable2.disaggregations.clear() + self.partneractivity_reportable.disaggregations.clear() + + # Create the disaggregations and values in the db for all response plans + # including one for no response plan as well + for disagg_name, values in self.sample_disaggregation_value_map.items(): + cluster_disagg = DisaggregationFactory(name=disagg_name, response_plan=self.response_plan) + + self.clusterobjective_reportable.disaggregations.add(cluster_disagg) + self.clusterobjective_reportable2.disaggregations.add(cluster_disagg) + self.partneractivity_reportable.disaggregations.add(cluster_disagg) + + for value in values: + DisaggregationValueFactory( + disaggregation=cluster_disagg, + value=value + ) + + super().setUp() + + # Logging in as Partner AO + self.client.force_authenticate(self.partner_user) + + def test_invalid_serializer_values(self): + url = reverse('partner-project-indicator-adopt') + + # Start with valid data payload + data = { + 'partner_id': self.partner.id, + 'partner_project_id': self.project.id, + 'cluster_id': self.cluster.id, + 'cluster_objective_id': self.objective.id, + 'reportable_id': self.clusterobjective_reportable.id, + 'locations': [ + { + 'id': self.reportable_loc_1.id, + 'baseline': {'d': 1, 'v': 1, 'c': 1}, + 'in_need': {'d': 1, 'v': 1, 'c': 1}, + 'target': {'d': 1, 'v': 1, 'c': 1}, + 'location': self.loc1.id, + }, + { + 'id': self.reportable_loc_2.id, + 'baseline': {'d': 1, 'v': 1, 'c': 1}, + 'in_need': {'d': 1, 'v': 1, 'c': 1}, + 'target': {'d': 1, 'v': 1, 'c': 1}, + 'location': self.loc2.id, + } + ], + 'target': {'d': 1, 'v': 1, 'c': 1}, + 'baseline': {'d': 1, 'v': 1, 'c': 1}, + } + + # Target value type check + data['target'] = list() + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Baseline value type check + data['target'] = {'d': 1, 'v': 1, 'c': 1} + data['baseline'] = list() + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # zero d value in target + data['baseline'] = {'d': 1, 'v': 1, 'c': 1} + data['target'] = {'d': 0, 'v': 1, 'c': 1} + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # missing v value in target + data['target'] = {'d': 1, 'c': 1} + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # zero d value in baseline + data['target'] = {'d': 1, 'v': 1, 'c': 1} + data['baseline'] = {'d': 0, 'v': 1, 'c': 1} + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # missing v value in baseline + data['baseline'] = {'d': 1, 'c': 1} + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Reset + data['target'] = {'d': 1, 'v': 1, 'c': 1} + data['baseline'] = {'d': 1, 'v': 1, 'c': 1} + + # Non-exist partner check + data['partner_id'] = 999999 + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Non-exist partner project check + data['partner_id'] = self.partner.id + data['partner_project_id'] = 999999 + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Invalid partner project membership check + data['partner_id'] = self.partner.id + data['partner_project_id'] = self.project2.id + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Non-exist cluster check + data['partner_project_id'] = self.project.id + data['cluster_id'] = 999999 + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Non-exist cluster objective check + data['cluster_id'] = self.cluster.id + data['cluster_objective_id'] = 999999 + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Invalid cluster objective membership check + data['cluster_id'] = self.cluster.id + data['cluster_objective_id'] = self.objective2.id + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Non-exist reportable_id check + data['cluster_objective_id'] = self.objective.id + data['reportable_id'] = 9999999 + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Invalid reportable type check + data['reportable_id'] = self.partneractivity_reportable.id + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) + + # Invalid reportable membership check + data['reportable_id'] = self.clusterobjective_reportable2.id + response = self.client.post(url, data=data, format='json') + self.assertTrue(status.is_client_error(response.status_code)) From 7281f04bdf1f46037f7f076a6bfd4fb58e50306d Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 2 May 2019 15:07:47 -0700 Subject: [PATCH 296/817] feat: Remove data going through processData method [ch10756] --- polymer/src/elements/cluster-reporting/indicator-modal.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 06c57df26..d6eb7fe4a 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -757,7 +757,7 @@

[[modalTitle]]

} this.set('data.partner_id', this.partnerID); this.set('data.cluster_id', selectedClusterId); - this.set('data.partner_project_id', this.objectId); + this.set('data.partner_project_id', parseInt(this.objectId)); this.debounce('fetch-objectives', function () { var self = this; @@ -888,7 +888,7 @@

[[modalTitle]]

delete location.loc_type; }); - this.$.indicators.body = this._processData(this.data); + this.$.indicators.body = this.data; console.log('objectives saved'); console.log('objectives url', this.$.indicators.url); From e915f87f23a6cdd250be76b618dc368d458b9abe Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 2 May 2019 15:07:57 -0700 Subject: [PATCH 297/817] fix: Remove negation on membership check validations [ch10756] --- django_api/django_api/apps/indicator/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_api/django_api/apps/indicator/serializers.py b/django_api/django_api/apps/indicator/serializers.py index d14bc196e..d4be9cc68 100644 --- a/django_api/django_api/apps/indicator/serializers.py +++ b/django_api/django_api/apps/indicator/serializers.py @@ -1144,7 +1144,7 @@ def validate(self, data): 'partner_project_id': 'PartnerProject does not exist' }) - if not PartnerProject.objects.get(id=data['partner_project_id']).partner.id != data['partner_id']: + if PartnerProject.objects.get(id=data['partner_project_id']).partner.id != data['partner_id']: raise serializers.ValidationError({ 'partner_project_id': 'This partner project does not belong to the partner' }) @@ -1159,7 +1159,7 @@ def validate(self, data): 'cluster_objective_id': 'ClusterObjective does not exist' }) - if not ClusterObjective.objects.get(id=data['cluster_objective_id']).cluster.id != data['cluster_id']: + if ClusterObjective.objects.get(id=data['cluster_objective_id']).cluster.id != data['cluster_id']: raise serializers.ValidationError({ 'cluster_objective_id': 'This objective does not belong to the cluster' }) From d3874ea201b22a8db582cd1e4fc60fb5ebfcedc9 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 2 May 2019 15:36:57 -0700 Subject: [PATCH 298/817] fix: Added --unsafe-perm and updated package version in ecstatic [ch10756] --- polymer/Dockerfile | 2 +- polymer/Dockerfile-bundle | 2 +- polymer/package.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/polymer/Dockerfile b/polymer/Dockerfile index d4da46fce..735276de9 100644 --- a/polymer/Dockerfile +++ b/polymer/Dockerfile @@ -18,7 +18,7 @@ RUN apk --update --no-cache \ libjpeg-turbo \ libjpeg-turbo-dev -RUN npm install -g bower polymer-cli gulp-cli +RUN npm install -g bower polymer-cli gulp-cli --unsafe-perm ADD bower.json /tmp/ ADD package.json /tmp/ diff --git a/polymer/Dockerfile-bundle b/polymer/Dockerfile-bundle index 4e926108b..f43be2f06 100644 --- a/polymer/Dockerfile-bundle +++ b/polymer/Dockerfile-bundle @@ -18,7 +18,7 @@ RUN apk --update --no-cache \ libjpeg-turbo \ libjpeg-turbo-dev -RUN npm install -g bower polymer-cli gulp-cli +RUN npm install -g bower polymer-cli gulp-cli --unsafe-perm ADD bower.json /tmp/ ADD package.json /tmp/ diff --git a/polymer/package.json b/polymer/package.json index 076f1dbed..8e6f96f42 100644 --- a/polymer/package.json +++ b/polymer/package.json @@ -25,6 +25,7 @@ "deep-extend": "0.5.1", "del": "^2.2.1", "fancy-log": "^1.3.2", + "ecstatic": "git+https://github.com/jfhbrook/node-ecstatic.git#7538ac5fda216c6ecf2a3386051feaf11f6deb2d", "gulp": "git://github.com/gulpjs/gulp.git#6d71a658c61edb3090221579d8f97dbe086ba2ed", "gulp-coverage": "^0.3.38", "gulp-csslint": "^1.0.0", @@ -40,7 +41,7 @@ "gulp-uglify": "^2.1.2", "gulp-useref": "^3.1.2", "htmlhint-stylish": "^1.0.3", - "http-server": "^0.10.0", + "http-server": "^0.11.1", "jasmine": "^3.4.0", "jasmine-ajax": "^4.0.0", "jshint": "^2.9.3", From b74475f8a44258191862e55785c5b432a00232e2 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 2 May 2019 16:07:06 -0700 Subject: [PATCH 299/817] feat: Make input fields change depending on indicator display type [ch10756] --- .../cluster-reporting/indicator-modal.html | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index d6eb7fe4a..ef61ef92f 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -197,25 +197,25 @@

[[modalTitle]]

- - + - - + + - + Custom @@ -245,200 +273,204 @@

[[modalTitle]]

is="dom-if" if="[[_equals(mode, 'custom')]]" restamp="true"> -
- - -
-
- - - Quantity (#) - Percent - Ratio - - -
- -
-
- - - - - - - - +
+
+ + +
+ +
+ + + Quantity (#) + Percent + Ratio +
-
- -
- - -
- -
- - -
- -
-
- - - - - - - + +
+
+ + + + + + + + + +
-
- -
- - - + +
+ +
+
- - - - + + + + + +
+
+ +
+ + +
+ +
+ +
- -
- - -
- -
- - -
+
@@ -454,7 +486,7 @@

[[modalTitle]]

+ on-tap="_close"> Cancel
@@ -951,6 +983,18 @@

[[modalTitle]]

}); } }, + + _close: function () { + console.log('closed!'); + this.set('mode', ''); + + this.set('selectedCluster', ''); + this.set('selectedObjective', ''); + this.set('selectedIndicator', ''); + this.set('selectedIndicatorDetailType', ''); + + this.close(); + }, }); From 5d3f8be01cb205ecaae9f7bda8b4be10d0d696e3 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 3 May 2019 12:20:51 -0700 Subject: [PATCH 310/817] feat: Have close event properly fire when modal is closed by any method [ch10756] --- .../src/elements/cluster-reporting/indicator-modal.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 8d1ee50b7..24bd71a5a 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -147,6 +147,7 @@
@@ -984,8 +985,11 @@

[[modalTitle]]

} }, - _close: function () { - console.log('closed!'); + _close: function (e) { + if (e.target.nodeName !== 'PAPER-DIALOG') { + return; + } + this.set('mode', ''); this.set('selectedCluster', ''); From ce379031dd95d1711078ada08930f2cb917c3336 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 3 May 2019 14:24:56 -0700 Subject: [PATCH 311/817] fix: Add conditional to check if cluster is null to have Overview render properly [ch10761] --- polymer/src/elements/etools-prp-permissions.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/polymer/src/elements/etools-prp-permissions.html b/polymer/src/elements/etools-prp-permissions.html index 3eecd0371..01f71d853 100644 --- a/polymer/src/elements/etools-prp-permissions.html +++ b/polymer/src/elements/etools-prp-permissions.html @@ -127,9 +127,14 @@ return entityClusters.some(function (cluster) { return params.prpRoles.some(function (item) { - return item.role === App.Constants.PRP_ROLE.CLUSTER_SYSTEM_ADMIN || - (String(item.cluster.id) === String(cluster.id) && - allowedRoles.indexOf(item.role) > -1); + if (item.cluster === null) { + return item.role === App.Constants.PRP_ROLE.CLUSTER_SYSTEM_ADMIN; + } else { + return item.role === App.Constants.PRP_ROLE.CLUSTER_SYSTEM_ADMIN || + (String(item.cluster.id) === String(cluster.id) && + allowedRoles.indexOf(item.role) > -1); + } + }); }); }, From c73b891651a0460111495440f00a84928a12ba3a Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 3 May 2019 14:46:48 -0700 Subject: [PATCH 312/817] fix: Add conditional for createPartnerEntitiesByResponsePlan to catch if cluster is null [ch10761] --- polymer/src/elements/etools-prp-permissions.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/polymer/src/elements/etools-prp-permissions.html b/polymer/src/elements/etools-prp-permissions.html index 01f71d853..0a86115e3 100644 --- a/polymer/src/elements/etools-prp-permissions.html +++ b/polymer/src/elements/etools-prp-permissions.html @@ -115,9 +115,13 @@ return responsePlanClusters.some(function (cluster) { return params.prpRoles.some(function (item) { - return item.role === App.Constants.PRP_ROLE.CLUSTER_SYSTEM_ADMIN || - (String(item.cluster.id) === String(cluster.id) && - allowedRoles.indexOf(item.role) > -1); + if (item.cluster === null) { + return item.role === App.Constants.PRP_ROLE.CLUSTER_SYSTEM_ADMIN; + } else { + return item.role === App.Constants.PRP_ROLE.CLUSTER_SYSTEM_ADMIN || + (String(item.cluster.id) === String(cluster.id) && + allowedRoles.indexOf(item.role) > -1); + } }); }); }, @@ -134,7 +138,6 @@ (String(item.cluster.id) === String(cluster.id) && allowedRoles.indexOf(item.role) > -1); } - }); }); }, From c92677c016065ab57731d52d9e3f4f884a552f2b Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 3 May 2019 14:55:35 -0700 Subject: [PATCH 313/817] style: Make sure styling is adjusted for non-number types in indicator modal [ch10761] --- .../cluster-reporting/indicator-modal.html | 92 ++++++++++++------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 24bd71a5a..72ea61ec5 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -73,7 +73,7 @@ padding-top: 0; } - .freq, .start { + .pair { margin-right: 0; } @@ -343,7 +343,7 @@

[[modalTitle]]

[[modalTitle]] [[modalTitle]] is="dom-if" if="[[!isNumber]]" restamp="true"> -
+
[[modalTitle]] [[modalTitle]]
-
- - - - +
+ +
+
From 24d8085b72c9c672f89d0e3dc3d0247d347d54bc Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 3 May 2019 15:15:38 -0700 Subject: [PATCH 314/817] fix: Reenable closing modal using exit and cancel buttons [ch10761] --- .../cluster-reporting/indicator-modal.html | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 72ea61ec5..b6e413e7c 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -1014,18 +1014,20 @@

[[modalTitle]]

}, _close: function (e) { - if (e.target.nodeName !== 'PAPER-DIALOG') { + if (e.target.nodeName === 'PAPER-DIALOG' || + e.target.nodeName === 'PAPER-BUTTON' || + e.target.nodeName === 'PAPER-ICON-BUTTON') { + this.set('mode', ''); + + this.set('selectedCluster', ''); + this.set('selectedObjective', ''); + this.set('selectedIndicator', ''); + this.set('selectedIndicatorDetailType', ''); + + this.close(); + } else { return; } - - this.set('mode', ''); - - this.set('selectedCluster', ''); - this.set('selectedObjective', ''); - this.set('selectedIndicator', ''); - this.set('selectedIndicatorDetailType', ''); - - this.close(); }, }); From 09e4602830a043dfb3205f073e3aeec1bbeca95a Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 3 May 2019 15:29:01 -0700 Subject: [PATCH 315/817] lint: flake8 fixes --- django_api/django_api/apps/indicator/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_api/django_api/apps/indicator/models.py b/django_api/django_api/apps/indicator/models.py index 2806da231..be50e192c 100644 --- a/django_api/django_api/apps/indicator/models.py +++ b/django_api/django_api/apps/indicator/models.py @@ -449,7 +449,7 @@ def create_reportable_for_pp_from_co_reportable(pp, co_reportable): Raises: ValidationError -- Django Exception - + Returns: Reportable -- PartnerProject type Reportable ORM instance """ From 7e6cff6d49e5e2c194fd24e75069e90a6a91d7b2 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 3 May 2019 15:29:35 -0700 Subject: [PATCH 316/817] lint: flake8 fixes --- django_api/django_api/apps/indicator/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/django_api/django_api/apps/indicator/views.py b/django_api/django_api/apps/indicator/views.py index 54c8ed71c..c140ef4db 100644 --- a/django_api/django_api/apps/indicator/views.py +++ b/django_api/django_api/apps/indicator/views.py @@ -36,7 +36,6 @@ PRP_ROLE_TYPES, ) from core.serializers import ShortLocationSerializer -from cluster.models import ClusterObjective from partner.models import PartnerProject from unicef.models import ProgressReport from unicef.permissions import UnicefPartnershipManagerOrRead From 56957df6fe57611abe663f21dc7e611c1d0dcc12 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 3 May 2019 15:37:22 -0700 Subject: [PATCH 317/817] lint: Fixed gulp JS lint errors --- polymer/src/endpoints.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymer/src/endpoints.html b/polymer/src/endpoints.html index 0d6f16b50..c23254edc 100644 --- a/polymer/src/endpoints.html +++ b/polymer/src/endpoints.html @@ -243,7 +243,7 @@ }, adoptedClusterIndicators: function () { - return this._buildUrl('/indicator/partner-project-indicator-adopt/') + return this._buildUrl('/indicator/partner-project-indicator-adopt/'); }, partnerActivity: function (responsePlanId, mode) { From 492302dcced6dc0e8f253194e545624076c70cdc Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 3 May 2019 15:38:13 -0700 Subject: [PATCH 318/817] lint: Fixed gulp JS lint errors --- polymer/src/elements/cluster-reporting/indicator-modal.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 24bd71a5a..5560282ef 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -860,7 +860,7 @@

[[modalTitle]]

this.$.indicatorDetail.abort(); this.$.indicatorDetail.thunk()() .then(function (res) { - self.set('selectedIndicatorDetailType', res.data.display_type) + self.set('selectedIndicatorDetailType', res.data.display_type); }) .catch(function (err) { // jshint ignore:line // TODO: error handling @@ -908,7 +908,7 @@

[[modalTitle]]

return; } - if(this.mode === 'objectives') { + if (this.mode === 'objectives') { this._setDisaggregations(); var data = this.get('data'); From d6db213b42dd0cede9c4a6a0c1283fc30e51b0e5 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Fri, 3 May 2019 16:21:52 -0700 Subject: [PATCH 319/817] fix: Check v key in target and baseline earlier than string type check --- django_api/django_api/apps/indicator/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/django_api/django_api/apps/indicator/serializers.py b/django_api/django_api/apps/indicator/serializers.py index fb2b91454..8f16c7089 100644 --- a/django_api/django_api/apps/indicator/serializers.py +++ b/django_api/django_api/apps/indicator/serializers.py @@ -1114,6 +1114,9 @@ def validate(self, data): else: raise serializers.ValidationError("key 'd' for target needs to be number") + if 'v' not in data['target']: + raise serializers.ValidationError("key 'v' must exist") + if isinstance(data['target']['v'], str): if data['target']['v'].isnumeric(): data['target']['v'] = int(data['target']['v']) @@ -1123,9 +1126,6 @@ def validate(self, data): if data['target']['d'] == 0: raise serializers.ValidationError("key 'd' cannot be zero") - if 'v' not in data['target']: - raise serializers.ValidationError("key 'v' must exist") - if 'c' not in data['target']: data['target']['c'] = float(data['target']['v']) / data['target']['d'] @@ -1143,6 +1143,9 @@ def validate(self, data): else: raise serializers.ValidationError("key 'd' for baseline needs to be number") + if 'v' not in data['baseline']: + raise serializers.ValidationError("key 'v' must exist") + if isinstance(data['baseline']['v'], str): if data['baseline']['v'].isnumeric(): data['baseline']['v'] = int(data['baseline']['v']) @@ -1152,9 +1155,6 @@ def validate(self, data): if data['baseline']['d'] == 0: raise serializers.ValidationError("key 'd' cannot be zero") - if 'v' not in data['baseline']: - raise serializers.ValidationError("key 'v' must exist") - if 'c' not in data['baseline']: data['baseline']['c'] = float(data['baseline']['v']) / data['baseline']['d'] From b6bea5b3bfe17f3650f4a2882e8d9d4f2d90a73d Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 10:08:41 -0700 Subject: [PATCH 320/817] fix: Unlocalize project title to have it display [ch10761] --- polymer/src/elements/page-header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polymer/src/elements/page-header.html b/polymer/src/elements/page-header.html index efc0ad2e7..1c3fe2c81 100644 --- a/polymer/src/elements/page-header.html +++ b/polymer/src/elements/page-header.html @@ -67,7 +67,7 @@
-

[[localize(title)]]

+

[[title]]

From 33f34e630cf312bb38247d0f4dd7396e8f1df758 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 6 May 2019 13:32:00 -0700 Subject: [PATCH 321/817] feat: Added composition step for react container [ch11331] --- .../codefresh_id_management_frontend.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/react/id_management_frontend/codefresh_id_management_frontend.yml b/react/id_management_frontend/codefresh_id_management_frontend.yml index afd41c8d0..d46014ac6 100644 --- a/react/id_management_frontend/codefresh_id_management_frontend.yml +++ b/react/id_management_frontend/codefresh_id_management_frontend.yml @@ -9,6 +9,14 @@ steps: working_directory: ${{main_clone}}/react/id_management_frontend tag: ${{CF_BRANCH}} + composition_step: + type: composition + composition: 'unicef-prp-unit-tests' + composition_candidates: + test: + image: ${{build_id_management_react}} + command: bash -c "npm run test" + push_to_hub: type: push title: Pushing id management react image to docker hub From f83c7275082b8e5b786418550901df76835170e9 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 15:06:54 -0700 Subject: [PATCH 322/817] feat: Add snapshot test for PlainButton and verify it passes [ch11331] --- .../components/common/test/ButtonNew.test.js | 1 - .../common/test/PaginatedList.test.js | 3 ++- .../common/test/PlainButton.test.js | 19 +++++++++++++++++++ .../__snapshots__/PlainButton.test.js.snap | 13 +++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 react/id_management_frontend/src/components/common/test/PlainButton.test.js create mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap diff --git a/react/id_management_frontend/src/components/common/test/ButtonNew.test.js b/react/id_management_frontend/src/components/common/test/ButtonNew.test.js index aed422f49..52941d07d 100644 --- a/react/id_management_frontend/src/components/common/test/ButtonNew.test.js +++ b/react/id_management_frontend/src/components/common/test/ButtonNew.test.js @@ -6,7 +6,6 @@ import ButtonNew from '../ButtonNew'; describe('ButtonNew component', () => { it('renders ButtonNew component and calls click', () => { const onClick = jest.fn(); - const classes = {icon: "ButtonNew-icon-1"}; const wrapper = shallow( { const columns = []; diff --git a/react/id_management_frontend/src/components/common/test/PlainButton.test.js b/react/id_management_frontend/src/components/common/test/PlainButton.test.js new file mode 100644 index 000000000..0356f1c55 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/PlainButton.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import PlainButton from '../PlainButton'; + +describe('PlainButton component', () => { + it('renders button and calls click', () => { + const children = {}; + const classes = {}; + + const wrapper = shallow() + + expect(wrapper.dive()).toBeTruthy(); + expect(toJSON(wrapper)).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap new file mode 100644 index 000000000..27321139f --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PlainButton component renders button and calls click 1`] = ` + + + +`; From 430f0c818068cd206fa21f276d33eb22d65207a0 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 16:01:19 -0700 Subject: [PATCH 323/817] feat: Add snapshot test for SmallText and verify it passes [ch11331] --- .../components/common/test/SmallText.test.js | 59 +++++++++++++++++++ .../test/__snapshots__/SmallText.test.js.snap | 17 ++++++ 2 files changed, 76 insertions(+) create mode 100644 react/id_management_frontend/src/components/common/test/SmallText.test.js create mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap diff --git a/react/id_management_frontend/src/components/common/test/SmallText.test.js b/react/id_management_frontend/src/components/common/test/SmallText.test.js new file mode 100644 index 000000000..46e8c604e --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/SmallText.test.js @@ -0,0 +1,59 @@ +import React from 'react'; +import {shallow, mount} from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import {Typography} from '@material-ui/core' +import SmallText from '../SmallText'; + +describe('SmallText component', () => { + const gutterBottom = false; + const children =
; + const classes = {block: 'block', text: 'text'}; + + it('renders the component', () => { + const block = false; + const label = false; + + const wrapper = shallow(); + + expect(wrapper.dive()).toBeTruthy(); + expect(toJSON(wrapper)).toMatchSnapshot(); + }); + + it('sets the color and className attributes correctly when label and block are true', () => { + const block = true; + const label = true; + + const wrapper = mount(); + + expect(wrapper.find(Typography).hasClass('block')).toBe(true); + expect(wrapper.find(Typography).prop('color')).toBe('default'); + }); + + it('sets the color and className attributes correctly when label and block are false', () => { + const block = false; + const label = false; + + const wrapper = mount(); + + expect(wrapper.find(Typography).hasClass('block')).toBe(false); + expect(wrapper.find(Typography).prop('color')).toBe('inherit'); + }); +}); \ No newline at end of file diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap new file mode 100644 index 000000000..9aa0f31c6 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SmallText component renders the component 1`] = ` + +
+ +`; From 823d489ed0d857cc433b4c5dea701565e271eee9 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 6 May 2019 16:08:08 -0700 Subject: [PATCH 324/817] fix: Added modulePathIgnorePatterns option for jest node settings [ch11331] --- react/id_management_frontend/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/react/id_management_frontend/package.json b/react/id_management_frontend/package.json index a92be390b..69ae272c6 100644 --- a/react/id_management_frontend/package.json +++ b/react/id_management_frontend/package.json @@ -106,6 +106,9 @@ "transformIgnorePatterns": [ "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$" ], + "modulePathIgnorePatterns": [ + "node_modules" + ], "moduleNameMapper": { "^react-native$": "react-native-web" }, From f1fafd3f849b6e267e2031575af901ca559f1b0c Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 16:18:44 -0700 Subject: [PATCH 325/817] feat: Add snapshot test for SmallValue and verify it passes [ch11331] --- .../components/common/test/SmallValue.test.js | 18 ++++++++++++++++++ .../test/__snapshots__/SmallValue.test.js.snap | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 react/id_management_frontend/src/components/common/test/SmallValue.test.js create mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap diff --git a/react/id_management_frontend/src/components/common/test/SmallValue.test.js b/react/id_management_frontend/src/components/common/test/SmallValue.test.js new file mode 100644 index 000000000..d5538bb05 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/SmallValue.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import SmallValue from '../SmallValue'; + +describe('SmallValue component', () => { + it('renders the component correctly', () => { + const label = 'CoolText'; + const value = 'CoolValue'; + + const wrapper = shallow(); + + expect(toJSON(wrapper)).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap new file mode 100644 index 000000000..0bb4a603e --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SmallValue component renders the component correctly 1`] = ` +
+ + CoolText + + + CoolValue + +
+`; From 17665eb0738422a3b9265600f0dc7bff3101c2a2 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Mon, 6 May 2019 16:28:00 -0700 Subject: [PATCH 326/817] fix: Updated composition command in codefresh for react [ch11331] + CI env variable + with coverage --- .../codefresh_id_management_frontend.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/react/id_management_frontend/codefresh_id_management_frontend.yml b/react/id_management_frontend/codefresh_id_management_frontend.yml index d46014ac6..ee9e0936b 100644 --- a/react/id_management_frontend/codefresh_id_management_frontend.yml +++ b/react/id_management_frontend/codefresh_id_management_frontend.yml @@ -15,7 +15,9 @@ steps: composition_candidates: test: image: ${{build_id_management_react}} - command: bash -c "npm run test" + command: bash -c "npm run test -- --coverage" + environment: + - CI=true push_to_hub: type: push From 8734830a5219cfc3774861d6eb1231994aebe4f7 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 16:34:17 -0700 Subject: [PATCH 327/817] feat: Add snapshot test for Snackbar and verify it passes [ch11331] --- .../components/common/test/Snackbar.test.js | 22 ++++++++++++++++++ .../test/__snapshots__/Snackbar.test.js.snap | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 react/id_management_frontend/src/components/common/test/Snackbar.test.js create mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap diff --git a/react/id_management_frontend/src/components/common/test/Snackbar.test.js b/react/id_management_frontend/src/components/common/test/Snackbar.test.js new file mode 100644 index 000000000..222aeb893 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/Snackbar.test.js @@ -0,0 +1,22 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import Snackbar from '../Snackbar'; + +describe('Snackbar component', () => { + it('renders the component properly', () => { + const message = 'This is a message!'; + const onClose = jest.fn(); + const open = false; + const variant = 'variant'; + + const wrapper = shallow(); + + expect(toJSON(wrapper)).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap new file mode 100644 index 000000000..2878926b4 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Snackbar component renders the component properly 1`] = ` +
+ + + +
+`; From 843838f1ef40cbdc67d6775a04c10bb4846ee070 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 17:06:32 -0700 Subject: [PATCH 328/817] feat: Add snapshot tests for SnackbarContentWrapper component [ch11331] --- .../common/SnackbarContentWrapper.jsx | 4 +- .../test/SnackbarContentWrapper.test.js | 43 +++++++++++++++++++ .../SnackbarContentWrapper.test.js.snap | 23 ++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 react/id_management_frontend/src/components/common/test/SnackbarContentWrapper.test.js create mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap diff --git a/react/id_management_frontend/src/components/common/SnackbarContentWrapper.jsx b/react/id_management_frontend/src/components/common/SnackbarContentWrapper.jsx index 0068736bb..0cae7fa95 100644 --- a/react/id_management_frontend/src/components/common/SnackbarContentWrapper.jsx +++ b/react/id_management_frontend/src/components/common/SnackbarContentWrapper.jsx @@ -38,9 +38,9 @@ function SnackbarContentWrapper(props) { aria-describedby="client-snackbar" message={ - + {message} - + } action={[ { + const classes = {}; + const className = 'coolClass'; + const message =

This is a message

; + const onClose = jest.fn(); + const variant = 'error'; + const other = {}; + + it('renders the component correctly', () => { + const wrapper = shallow(); + + expect(toJSON(wrapper)).toMatchSnapshot(); + }); + + it('calls onClick correctly', () => { + const wrapper = mount(); + + wrapper.find(IconButton).simulate('click'); + const calls = onClose.mock.calls; + + expect(calls.length).toBe(1); + }); +}); \ No newline at end of file diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap new file mode 100644 index 000000000..6ea205702 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SnackbarContentWrapper component renders the component correctly 1`] = ` + + This is a message +

+ } + onClose={[Function]} + other={Object {}} + variant="error" +/> +`; From f1b90d4844851870b87e19655b6fc56fbf588fbe Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 6 May 2019 17:10:28 -0700 Subject: [PATCH 329/817] fix: Delete snapshots and add snapshot folder to gitignore [ch11331] --- react/id_management_frontend/.gitignore | 1 + .../__snapshots__/ButtonClear.test.js.snap | 10 - .../test/__snapshots__/ButtonNew.test.js.snap | 12 - .../__snapshots__/ButtonSubmit.test.js.snap | 338 ------------------ .../__snapshots__/ConfirmDialog.test.js.snap | 16 - .../__snapshots__/DeleteButton.test.js.snap | 13 - .../test/__snapshots__/Dialog.test.js.snap | 265 -------------- .../__snapshots__/DialogActions.test.js.snap | 13 - .../FieldLabelWithTooltip.test.js.snap | 19 - .../FieldsArrayAddButton.test.js.snap | 12 - .../FieldsArrayItem.test.js.snap | 13 - .../FieldsArrayPanel.test.js.snap | 15 - .../__snapshots__/FilterButtons.test.js.snap | 12 - .../__snapshots__/FilterInput.test.js.snap | 15 - .../test/__snapshots__/GreyPanel.test.js.snap | 13 - .../__snapshots__/LinkButton.test.js.snap | 15 - .../LoadingIndicator.test.js.snap | 12 - .../__snapshots__/MenuSelect.test.js.snap | 35 -- .../__snapshots__/PaddedContent.test.js.snap | 16 - .../__snapshots__/PageContent.test.js.snap | 13 - .../__snapshots__/PageHeader.test.js.snap | 13 - .../__snapshots__/PaginatedList.test.js.snap | 59 --- .../__snapshots__/PlainButton.test.js.snap | 13 - .../test/__snapshots__/SmallText.test.js.snap | 17 - .../__snapshots__/SmallValue.test.js.snap | 17 - .../test/__snapshots__/Snackbar.test.js.snap | 23 -- .../SnackbarContentWrapper.test.js.snap | 23 -- 27 files changed, 1 insertion(+), 1022 deletions(-) delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/ButtonClear.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/ButtonNew.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/ButtonSubmit.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/ConfirmDialog.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/DeleteButton.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/Dialog.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/DialogActions.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/FieldLabelWithTooltip.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayAddButton.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayItem.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayPanel.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/FilterButtons.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/FilterInput.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/GreyPanel.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/LinkButton.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/LoadingIndicator.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/MenuSelect.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/PaddedContent.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/PageContent.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/PageHeader.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/PaginatedList.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap delete mode 100644 react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap diff --git a/react/id_management_frontend/.gitignore b/react/id_management_frontend/.gitignore index d30f40ef4..d56097d38 100644 --- a/react/id_management_frontend/.gitignore +++ b/react/id_management_frontend/.gitignore @@ -5,6 +5,7 @@ # testing /coverage +/**/__snapshots__ # production /build diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonClear.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonClear.test.js.snap deleted file mode 100644 index 450d99ad9..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonClear.test.js.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Clear button component renders clear button and calls click 1`] = ` - - Clear - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonNew.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonNew.test.js.snap deleted file mode 100644 index e8d7ceacc..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonNew.test.js.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ButtonNew component renders ButtonNew component and calls click 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonSubmit.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonSubmit.test.js.snap deleted file mode 100644 index 6b16fa6d6..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/ButtonSubmit.test.js.snap +++ /dev/null @@ -1,338 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ButtonSubmit component renders ButtonSubmit component and calls click properly 1`] = ` - - -
- - - - - - -
-
-
-`; - -exports[`ButtonSubmit component renders Loading component when loading is true 1`] = ` - - -
- - - - - - - - -
- - - -
-
-
-
-
-
-`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/ConfirmDialog.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/ConfirmDialog.test.js.snap deleted file mode 100644 index 0bb591794..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/ConfirmDialog.test.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ConfirmDialog component renders component properly 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/DeleteButton.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/DeleteButton.test.js.snap deleted file mode 100644 index e46b018ba..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/DeleteButton.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DeleteButton component renders component properly and calls click 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/Dialog.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/Dialog.test.js.snap deleted file mode 100644 index bc23ecc67..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/Dialog.test.js.snap +++ /dev/null @@ -1,265 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Dialog component renders component correctly and calls click 1`] = ` - - - - - - - - - - - -`; - -exports[`Dialog component renders properly when loading is false 1`] = ` - - - - - - - - - - - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/DialogActions.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/DialogActions.test.js.snap deleted file mode 100644 index 97b81cfaf..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/DialogActions.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DialogActions component renders properly 1`] = ` - - - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldLabelWithTooltip.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/FieldLabelWithTooltip.test.js.snap deleted file mode 100644 index 7984e2466..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldLabelWithTooltip.test.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldLabelWithTooltip component renders properly 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayAddButton.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayAddButton.test.js.snap deleted file mode 100644 index dae9bbe45..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayAddButton.test.js.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldsArrayAddButton component renders component properly and calls click 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayItem.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayItem.test.js.snap deleted file mode 100644 index 8d279b5c6..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayItem.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldsArrayItem component renders the component 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayPanel.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayPanel.test.js.snap deleted file mode 100644 index eadac4069..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/FieldsArrayPanel.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldsArrayPanel component renders the component 1`] = ` - -

- Hello -

-
-`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/FilterButtons.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/FilterButtons.test.js.snap deleted file mode 100644 index ed9ec86e7..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/FilterButtons.test.js.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FilterButtons component renders component properly 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/FilterInput.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/FilterInput.test.js.snap deleted file mode 100644 index 382d73242..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/FilterInput.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FilterInput component renders properly 1`] = ` - - - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/GreyPanel.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/GreyPanel.test.js.snap deleted file mode 100644 index a1aa2747b..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/GreyPanel.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GreyPanel component renders properly 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/LinkButton.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/LinkButton.test.js.snap deleted file mode 100644 index a08557dc9..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/LinkButton.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LinkButton component renders properly 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/LoadingIndicator.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/LoadingIndicator.test.js.snap deleted file mode 100644 index bd4b53b84..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/LoadingIndicator.test.js.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LoadingIndicator component renders the component 1`] = ` - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/MenuSelect.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/MenuSelect.test.js.snap deleted file mode 100644 index b13668f8c..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/MenuSelect.test.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MenuSelect component renders properly 1`] = ` - - - test - - - - - None - - - - dreamy - - - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/PaddedContent.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/PaddedContent.test.js.snap deleted file mode 100644 index d2afbb743..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/PaddedContent.test.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PaddedContent component renders properly 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/PageContent.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/PageContent.test.js.snap deleted file mode 100644 index 2ad7b136e..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/PageContent.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PageContent component renders properly 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/PageHeader.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/PageHeader.test.js.snap deleted file mode 100644 index e3c7cfa98..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/PageHeader.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PageHeader component renders the component 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/PaginatedList.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/PaginatedList.test.js.snap deleted file mode 100644 index c0eea51d2..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/PaginatedList.test.js.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PaginatedList component renders the component 1`] = ` - - -
- - NaN-NaN of undefined results to show - -
- - - - - - - - -
-
-`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap deleted file mode 100644 index 27321139f..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/PlainButton.test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PlainButton component renders button and calls click 1`] = ` - - - -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap deleted file mode 100644 index 9aa0f31c6..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/SmallText.test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SmallText component renders the component 1`] = ` - -
- -`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap deleted file mode 100644 index 0bb4a603e..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/SmallValue.test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SmallValue component renders the component correctly 1`] = ` -
- - CoolText - - - CoolValue - -
-`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap deleted file mode 100644 index 2878926b4..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/Snackbar.test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Snackbar component renders the component properly 1`] = ` -
- - - -
-`; diff --git a/react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap b/react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap deleted file mode 100644 index 6ea205702..000000000 --- a/react/id_management_frontend/src/components/common/test/__snapshots__/SnackbarContentWrapper.test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SnackbarContentWrapper component renders the component correctly 1`] = ` - - This is a message -

- } - onClose={[Function]} - other={Object {}} - variant="error" -/> -`; From 21105783ec5560fbc2c91f56162edcf737b2833e Mon Sep 17 00:00:00 2001 From: Silin Na Date: Tue, 7 May 2019 09:50:02 -0700 Subject: [PATCH 330/817] fix: Added --updateSnapshot argument to jest [ch11331] --- .../id_management_frontend/codefresh_id_management_frontend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/id_management_frontend/codefresh_id_management_frontend.yml b/react/id_management_frontend/codefresh_id_management_frontend.yml index ee9e0936b..53e166e1f 100644 --- a/react/id_management_frontend/codefresh_id_management_frontend.yml +++ b/react/id_management_frontend/codefresh_id_management_frontend.yml @@ -15,7 +15,7 @@ steps: composition_candidates: test: image: ${{build_id_management_react}} - command: bash -c "npm run test -- --coverage" + command: bash -c "npm run test -- --coverage --updateSnapshot" environment: - CI=true From aed6440455ff61c65d8466680a23f3888864bd26 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Tue, 7 May 2019 12:09:52 -0700 Subject: [PATCH 331/817] fix: Changed narrative field in IndicatorReport as TextField [ch11490] + DB migration --- .../migrations/0003_auto_20190507_1908.py | 20 +++++++++++++++++++ .../django_api/apps/indicator/models.py | 4 +--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 django_api/django_api/apps/indicator/migrations/0003_auto_20190507_1908.py diff --git a/django_api/django_api/apps/indicator/migrations/0003_auto_20190507_1908.py b/django_api/django_api/apps/indicator/migrations/0003_auto_20190507_1908.py new file mode 100644 index 000000000..6e0f8c240 --- /dev/null +++ b/django_api/django_api/apps/indicator/migrations/0003_auto_20190507_1908.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-07 19:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('indicator', '0002_reportablelocationgoal_is_active'), + ] + + operations = [ + migrations.AlterField( + model_name='indicatorreport', + name='narrative_assessment', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/django_api/django_api/apps/indicator/models.py b/django_api/django_api/apps/indicator/models.py index be50e192c..93a843bac 100644 --- a/django_api/django_api/apps/indicator/models.py +++ b/django_api/django_api/apps/indicator/models.py @@ -600,9 +600,7 @@ class IndicatorReport(TimeStampedModel): default=OVERALL_STATUS.no_status, max_length=3 ) - narrative_assessment = models.CharField(max_length=255, - null=True, - blank=True) + narrative_assessment = models.TextField(null=True, blank=True) review_date = models.DateField(verbose_name='Review Date', blank=True, From 2752ec1e742ae826f653bd399d55774559899607 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 7 May 2019 14:55:00 -0700 Subject: [PATCH 332/817] fix: Add conditional to check for spaces in filename [ch11495] --- .../src/elements/ip-reporting/report-attachments.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index dad00eac8..41515f874 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -253,6 +253,15 @@ } var files = isEmpty ? [] : change.base; + console.log('all files', change.base); + + files.forEach(function (file) { + if (/\s/.test(file.file_name)) { + console.log('here!'); + file.file_name = file.file_name.replace(/ /g, '_'); + console.log('new name', file.file_name); + } + }) var attachmentType = attachmentPropertyName.toLowerCase().indexOf('face') !== -1 ? 'FACE' : 'Other'; From ee4e4df7061bfa5ddedfc8ed1d097cd0ff32cdd3 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 7 May 2019 16:03:36 -0700 Subject: [PATCH 333/817] fix: Add max_length attribute to serializers and models to accommodate longer file names [ch11495] --- .../migrations/0012_auto_20190507_2301.py | 21 +++++++++++++++++++ django_api/django_api/apps/unicef/models.py | 1 + .../django_api/apps/unicef/serializers.py | 2 +- .../ip-reporting/report-attachments.html | 5 +---- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 django_api/django_api/apps/unicef/migrations/0012_auto_20190507_2301.py diff --git a/django_api/django_api/apps/unicef/migrations/0012_auto_20190507_2301.py b/django_api/django_api/apps/unicef/migrations/0012_auto_20190507_2301.py new file mode 100644 index 000000000..ba52a2618 --- /dev/null +++ b/django_api/django_api/apps/unicef/migrations/0012_auto_20190507_2301.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-07 23:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import unicef.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('unicef', '0011_auto_20190418_1756'), + ] + + operations = [ + migrations.AlterField( + model_name='progressreportattachment', + name='file', + field=models.FileField(max_length=500, upload_to=unicef.models.get_pr_attachment_upload_to), + ), + ] diff --git a/django_api/django_api/apps/unicef/models.py b/django_api/django_api/apps/unicef/models.py index 0f876aba1..fe0d44f26 100644 --- a/django_api/django_api/apps/unicef/models.py +++ b/django_api/django_api/apps/unicef/models.py @@ -532,6 +532,7 @@ class ProgressReportAttachment(TimeStampedModel): progress_report = models.ForeignKey('unicef.ProgressReport', related_name="attachments") file = models.FileField( upload_to=get_pr_attachment_upload_to, + max_length=500 ) type = models.CharField(verbose_name="Attachment type", choices=PR_ATTACHMENT_TYPES, max_length=5) diff --git a/django_api/django_api/apps/unicef/serializers.py b/django_api/django_api/apps/unicef/serializers.py index 3af0580d1..7cffae2cc 100644 --- a/django_api/django_api/apps/unicef/serializers.py +++ b/django_api/django_api/apps/unicef/serializers.py @@ -788,7 +788,7 @@ class Meta: class ProgressReportAttachmentSerializer(serializers.ModelSerializer): size = serializers.SerializerMethodField() file_name = serializers.SerializerMethodField() - path = serializers.FileField(source='file') + path = serializers.FileField(source='file', max_length=500) def get_file_name(self, obj): return obj.file.name.split('/')[-1] if obj.file else None diff --git a/polymer/src/elements/ip-reporting/report-attachments.html b/polymer/src/elements/ip-reporting/report-attachments.html index 41515f874..de1ead292 100644 --- a/polymer/src/elements/ip-reporting/report-attachments.html +++ b/polymer/src/elements/ip-reporting/report-attachments.html @@ -253,15 +253,12 @@ } var files = isEmpty ? [] : change.base; - console.log('all files', change.base); files.forEach(function (file) { if (/\s/.test(file.file_name)) { - console.log('here!'); file.file_name = file.file_name.replace(/ /g, '_'); - console.log('new name', file.file_name); } - }) + }); var attachmentType = attachmentPropertyName.toLowerCase().indexOf('face') !== -1 ? 'FACE' : 'Other'; From 4ce3852eadf7da2d54611f8dcae15bd48a88825e Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 7 May 2019 17:15:57 -0700 Subject: [PATCH 334/817] fix: Have narrative assessment field save when navigating away from tab [ch11484] --- polymer/src/elements/reportable-meta.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/polymer/src/elements/reportable-meta.html b/polymer/src/elements/reportable-meta.html index 76b7355d1..08042f8b3 100644 --- a/polymer/src/elements/reportable-meta.html +++ b/polymer/src/elements/reportable-meta.html @@ -282,6 +282,9 @@ }, detached: function () { + this.set(['localData', 'narrative_assessment'], + Polymer.dom(this.root).querySelectorAll('labelled-item')[1].querySelector('input').value); + if (this.isDebouncerActive('local-data-changed')) { this.cancelDebouncer('local-data-changed'); } From 68a19a6e95dd5a56690e5850618adc34fcb1dd34 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 13:42:38 -0700 Subject: [PATCH 335/817] fix: Set language to en if navigator.language is not in available languages --- polymer/src/elements/language-dropdown.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/polymer/src/elements/language-dropdown.html b/polymer/src/elements/language-dropdown.html index f3cfdfefa..e1f3fbd07 100644 --- a/polymer/src/elements/language-dropdown.html +++ b/polymer/src/elements/language-dropdown.html @@ -92,6 +92,11 @@ computed: '_computeLanguage(data, current)', }, + availableLanguages: { + type: Object, + statePath: 'localize.resources', + }, + selected: { type: Number, value: 0, @@ -103,6 +108,16 @@ }, _languageSelected: function(e) { + console.log('all languages', this.availableLanguages); + console.log('current', this.current); + var allLanguages = Object.keys(this.availableLanguages); + console.log('list', allLanguages); + + if (allLanguages.includes(this.current) === false) { + console.log('not found!'); + // this.set('current', 'en'); + } + var newLanguage = this.$.repeat.itemForElement(e.detail.item); if (newLanguage === this.current) { From fc13dbd4fbd4bc57dbb53c63df2bb2dc2a004d3d Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 13:57:48 -0700 Subject: [PATCH 336/817] fix: Have Redux set language to en if browser language is not available and clean up console messages --- polymer/src/elements/language-dropdown.html | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/polymer/src/elements/language-dropdown.html b/polymer/src/elements/language-dropdown.html index e1f3fbd07..1eb40b34f 100644 --- a/polymer/src/elements/language-dropdown.html +++ b/polymer/src/elements/language-dropdown.html @@ -108,14 +108,10 @@ }, _languageSelected: function(e) { - console.log('all languages', this.availableLanguages); - console.log('current', this.current); var allLanguages = Object.keys(this.availableLanguages); - console.log('list', allLanguages); if (allLanguages.includes(this.current) === false) { - console.log('not found!'); - // this.set('current', 'en'); + this.dispatch(App.Actions.Localize.set('en')); } var newLanguage = this.$.repeat.itemForElement(e.detail.item); From ca791d149dc4b5e52c8820c373c8ded6fa5eca80 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 14:44:33 -0700 Subject: [PATCH 337/817] feat: Add snapshot test for TooltipIcon and verify full coverage [ch11331] --- .../common/test/TooltipIcon.test.js | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 react/id_management_frontend/src/components/common/test/TooltipIcon.test.js diff --git a/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js b/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js new file mode 100644 index 000000000..b49505cb8 --- /dev/null +++ b/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import {shallow} from 'enzyme'; +import toJSON from 'enzyme-to-json'; +import TooltipIcon from '../TooltipIcon'; + +describe('TooltipIcon component', () => { + it('renders the component', () => { + const classes = {}; + const icon = jest.fn(); + const infoText =

I am text

; + const iconClass = 'icon'; + const displayTooltip = false; + const name = 'name'; + const tooltipProps = {}; + + const wrapper = shallow(); + + expect(wrapper.dive()).toBeTruthy(); + expect(toJSON(wrapper)).toMatchSnapshot(); + }); +}); \ No newline at end of file From 02f4242602bc20f61cd05dd1311c4f01d73d0727 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 14:51:19 -0700 Subject: [PATCH 338/817] fix: Change test for TooltipIcon to specifically look at length [ch11331] --- .../src/components/common/test/TooltipIcon.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js b/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js index b49505cb8..c10189722 100644 --- a/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js +++ b/react/id_management_frontend/src/components/common/test/TooltipIcon.test.js @@ -23,7 +23,7 @@ describe('TooltipIcon component', () => { tooltipProps={tooltipProps} />); - expect(wrapper.dive()).toBeTruthy(); + expect(wrapper.dive().length).toBe(1); expect(toJSON(wrapper)).toMatchSnapshot(); }); }); \ No newline at end of file From 11c20f6b82c2c18a35e6ff091dd1188d6b29ad95 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 15:22:43 -0700 Subject: [PATCH 339/817] feat: Add snapshot test for mapDispatchToProps function in PaginatedList [ch11331] --- .../components/common/test/PaginatedList.test.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js index 013b714f2..5889d2b7e 100644 --- a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js +++ b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js @@ -1,8 +1,9 @@ import React from 'react'; -import {shallow} from 'enzyme'; +import {shallow, mount} from 'enzyme'; import toJSON from 'enzyme-to-json'; import {PaginatedList, - mapStateToProps} from '../PaginatedList'; + mapStateToProps, + mapDispatchToProps} from '../PaginatedList'; describe('PaginatedList component', () => { const columns = []; @@ -58,4 +59,13 @@ describe('PaginatedList component', () => { expect(map).toEqual(state); }); + + it('runs mapDispatchToProps correctly', () => { + const test = item => item; + const id = 5; + const disp = mapDispatchToProps(test); + const result = {'ids': 5, 'type': 'EXPANDED_ROW_IDS'}; + + expect(disp.dispatchExpandedRowIds(id)).toEqual(result); + }); }); \ No newline at end of file From 22eab24335d3609d8b67e472fb3b9628a80791d1 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 16:06:25 -0700 Subject: [PATCH 340/817] feat: Add snapshot test for onPageChange function in PaginatedList [ch11331] --- .../common/test/PaginatedList.test.js | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js index 5889d2b7e..eb323a5b0 100644 --- a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js +++ b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js @@ -1,6 +1,7 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; import toJSON from 'enzyme-to-json'; +import {PagingState} from '@devexpress/dx-react-grid'; import {PaginatedList, mapStateToProps, mapDispatchToProps} from '../PaginatedList'; @@ -9,7 +10,7 @@ describe('PaginatedList component', () => { const columns = []; const columnExtensions = []; const alternativeSorting = []; - const data = {}; + const data = {results: []}; const expandedCell = jest.fn(); const page = 451; const onPageChange = jest.fn(); @@ -61,11 +62,38 @@ describe('PaginatedList component', () => { }); it('runs mapDispatchToProps correctly', () => { - const test = item => item; + const dispatch = item => item; const id = 5; - const disp = mapDispatchToProps(test); + const dispatched = mapDispatchToProps(dispatch); const result = {'ids': 5, 'type': 'EXPANDED_ROW_IDS'}; - expect(disp.dispatchExpandedRowIds(id)).toEqual(result); + expect(dispatched.dispatchExpandedRowIds(id)).toEqual(result); + }); + + it('runs onPageChange correctly', () => { + const wrapper = mount(); + + expect(wrapper.find(PagingState).prop('currentPage')).toBe(450); + wrapper.find(PagingState).prop('onCurrentPageChange')(); + + const calls = onPageChange.mock.calls; + + expect(calls.length).toBe(1); }); }); \ No newline at end of file From 1d1947d792f9e1951a742bfea9705c25bf8566cc Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 8 May 2019 16:13:30 -0700 Subject: [PATCH 341/817] feat: Add snapshot test for expandedCell fucntion in PaginatedList [ch11331] --- .../common/test/PaginatedList.test.js | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js index eb323a5b0..f0ea54821 100644 --- a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js +++ b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js @@ -2,6 +2,7 @@ import React from 'react'; import {shallow, mount} from 'enzyme'; import toJSON from 'enzyme-to-json'; import {PagingState} from '@devexpress/dx-react-grid'; +import {TableRowDetail} from "@devexpress/dx-react-grid-material-ui"; import {PaginatedList, mapStateToProps, mapDispatchToProps} from '../PaginatedList'; @@ -70,25 +71,7 @@ describe('PaginatedList component', () => { expect(dispatched.dispatchExpandedRowIds(id)).toEqual(result); }); - it('runs onPageChange correctly', () => { - const wrapper = mount(); - + it('runs onPageChange function when onCurrentPageChange prop is called', () => { expect(wrapper.find(PagingState).prop('currentPage')).toBe(450); wrapper.find(PagingState).prop('onCurrentPageChange')(); @@ -96,4 +79,14 @@ describe('PaginatedList component', () => { expect(calls.length).toBe(1); }); + + it('runs expandedCell function when contentComponent prop is called', () => { + const row = {row: 5}; + + wrapper.find(TableRowDetail).prop('contentComponent')({row}); + + const calls = expandedCell.mock.calls; + + expect(calls.length).toBe(1); + }); }); \ No newline at end of file From 344112befceb734f3d21ad2c5557640403f34e39 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 9 May 2019 10:32:05 -0700 Subject: [PATCH 342/817] feat: Add snapshot test for computeInnerSorting functionality [ch11331] --- .../common/test/PaginatedList.test.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js index f0ea54821..23c511e25 100644 --- a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js +++ b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js @@ -10,7 +10,11 @@ import {PaginatedList, describe('PaginatedList component', () => { const columns = []; const columnExtensions = []; - const alternativeSorting = []; + const alternativeSorting = [ + {orderingNames: ['Levine', 'Rowling']}, + {orderingNames: ['Pope', 'Ma']}, + {orderingNames: ['Snicket', 'Davis']}, + ]; const data = {results: []}; const expandedCell = jest.fn(); const page = 451; @@ -24,10 +28,12 @@ describe('PaginatedList component', () => { const showEdit = false; const onExpandedRowIdsChange = jest.fn(); const classes = {}; + const sorting = [{columnName: 'Druckmann'}, {columnName: 'Pope'}, {columnName: 'Miyazaki'}]; const wrapper = shallow( { showEdit={showEdit} onExpandedRowIdsChange={onExpandedRowIdsChange} classes={classes} + sorting={sorting} />); it('renders the component', () => { @@ -84,9 +91,15 @@ describe('PaginatedList component', () => { const row = {row: 5}; wrapper.find(TableRowDetail).prop('contentComponent')({row}); - const calls = expandedCell.mock.calls; expect(calls.length).toBe(1); }); + + it('runs computeInnerSorting correctly when alternativeSorting is truthy', () => { + const instance = wrapper.instance(); + + const sorted = instance.computeInnerSorting(sorting); + expect(sorted).toEqual([{columnName: 'Druckmann'}, {columnName: 'Miyazaki'}]); + }); }); \ No newline at end of file From aff375da187c7f8bb85eff72bcc3e1761415dd30 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 9 May 2019 10:33:59 -0700 Subject: [PATCH 343/817] feat: Make additional snapshot test for computeInnerSorting function [ch11331] --- .../common/test/PaginatedList.test.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js index 23c511e25..4ea5f9116 100644 --- a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js +++ b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js @@ -102,4 +102,30 @@ describe('PaginatedList component', () => { const sorted = instance.computeInnerSorting(sorting); expect(sorted).toEqual([{columnName: 'Druckmann'}, {columnName: 'Miyazaki'}]); }); + + it('runs computeInnerSorting correctly wnen alternativeSorting is falsy', () => { + const wrapper = shallow(); + + const instance = wrapper.instance(); + const sorted = instance.computeInnerSorting(sorting); + + expect(sorted).toEqual(sorted); + }); }); \ No newline at end of file From 932d57a991424fe468e0f6d00f321c27eefa127b Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 9 May 2019 10:57:42 -0700 Subject: [PATCH 344/817] feat: Make another snapshot test for computeInnerSorting function [ch11331] --- .../common/test/PaginatedList.test.js | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js index 4ea5f9116..870b3b975 100644 --- a/react/id_management_frontend/src/components/common/test/PaginatedList.test.js +++ b/react/id_management_frontend/src/components/common/test/PaginatedList.test.js @@ -28,7 +28,7 @@ describe('PaginatedList component', () => { const showEdit = false; const onExpandedRowIdsChange = jest.fn(); const classes = {}; - const sorting = [{columnName: 'Druckmann'}, {columnName: 'Pope'}, {columnName: 'Miyazaki'}]; + const sorting = [{columnName: 'Druckmann'}, {columnName: 'Pope', direction: 'Dinn'}, {columnName: 'Miyazaki'}]; const wrapper = shallow( { expect(sorted).toEqual(sorted); }); + + it('runs computeInnerSorting correctly when length for alternativeSorting items are 1', () => { + const alternativeSorting = [ + {orderingNames: ['Levine']}, + {orderingNames: ['Pope'], columnName: 'Lucas'}, + {orderingNames: ['Snicket']}, + ]; + + const wrapper = shallow(); + + const instance = wrapper.instance(); + const sorted = instance.computeInnerSorting(sorting); + + expect(sorted).toEqual([{columnName: 'Druckmann'}, {columnName: 'Lucas', direction: 'Dinn'}, {columnName: 'Miyazaki'}]); + }); }); \ No newline at end of file From 49cd81336c600ae85f8c0445cee51b6769d3b9d9 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 9 May 2019 11:06:42 -0700 Subject: [PATCH 345/817] feat: Add official French translations from UNICEF [ch11929] --- polymer/src/locales.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/polymer/src/locales.json b/polymer/src/locales.json index f1ad55a1f..69ff08af3 100644 --- a/polymer/src/locales.json +++ b/polymer/src/locales.json @@ -21,24 +21,24 @@ "knowledge_base": "Knowledge Base" }, "fr": { - "ip_reporting": "Rapport de IP", - "cluster_reporting": "Rapport de Cluster", - "change": "change", - "profile": "Le profil", + "ip_reporting": "Reporting Partenaires UNICEF", + "cluster_reporting": "Reporting Cluster", + "change": "Changer", + "profile": "Profile", "sign_in": "Se connecter", "sign_out": "Déconnexion", "dashboard": "Tableau de bord", "response_parameters": "Paramètres de réponse", - "clusters": "Les clusters", - "partners": "Les partenaires", - "my_planned_action": "Mon action prévue", - "reporting_results": "Rapport des résultats", - "analysis": "L'analyse", + "clusters": "Clusters", + "partners": "Partenaires", + "my_planned_action": "Actions planifiées", + "reporting_results": "Reporting sur les résultats", + "analysis": "Analyse", "overview": "Vue d'ensemble", - "programme_documents": "Documents de programme", - "progress_reports": "Rapports d'avancement", - "indicators": "Des indicateurs", - "id_management": "Gestion des identifiants", + "programme_documents": "Documents de Programme", + "progress_reports": "Rapport de progrès", + "indicators": "Indicateurs", + "id_management": "Gestion d'identité", "knowledge_base": "Base de connaissances" } } \ No newline at end of file From e74afb41dfa28158b8ed33cf0deddff28756d722 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 9 May 2019 11:22:19 -0700 Subject: [PATCH 346/817] feat: Update IP Reporting pages to use localization for titles [ch11929] --- polymer/src/pages/app/ip-reporting/indicators.html | 6 +++++- polymer/src/pages/app/ip-reporting/overview.html | 10 +++++++++- polymer/src/pages/app/ip-reporting/pd/pd-index.html | 10 +++++++++- .../src/pages/app/ip-reporting/progress-reports.html | 6 +++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/polymer/src/pages/app/ip-reporting/indicators.html b/polymer/src/pages/app/ip-reporting/indicators.html index c53764e72..3f139e0df 100644 --- a/polymer/src/pages/app/ip-reporting/indicators.html +++ b/polymer/src/pages/app/ip-reporting/indicators.html @@ -2,6 +2,7 @@ + @@ -13,6 +14,7 @@ + @@ -299,6 +299,7 @@

Locations ([[value.length]])

parentIndicatorId: Number, clusterId: Number, locations: Object, + savedLocations: Object, pending: Object, isPai: Boolean, searchLocationType: String, @@ -478,6 +479,7 @@

Locations ([[value.length]])

this.set('valueInitialized', true); this.async(function () { + console.log('VALUE', value); this.set('lockedItems', value.slice()); }); }, @@ -513,9 +515,9 @@

Locations ([[value.length]])

}, _fetchLocations: function (loc_type, title) { - // if (this.get('locations')[loc_type]) { - // return; - // } + if (this.get('locations')[loc_type]) { + return; + } this.set('searchLocationType', loc_type); @@ -532,6 +534,7 @@

Locations ([[value.length]])

.then(function (res) { self.set('url', res.xhr.responseURL); self._setPending(loc_type, false); + console.log('res results', res.data.results); self._setLocations(loc_type, res.data.results); }) .catch(function () { @@ -541,7 +544,6 @@

Locations ([[value.length]])

}, _fetchInitialLocations: function (lockedItems) { - var self = this; var existingLocations = {}; lockedItems.forEach(function (item) { @@ -552,9 +554,15 @@

Locations ([[value.length]])

existingLocations[item.admin_level] = [item]; } - self.set('locations', existingLocations); + this.set('savedLocations', existingLocations); } else { - self._fetchLocations(item.loc_type, item.title); + if (existingLocations[item.loc_type]) { + existingLocations[item.loc_type].push(item); + } else { + existingLocations[item.loc_type] = [item]; + } + + this.set('savedLocations', existingLocations); } }, this); }, @@ -567,9 +575,16 @@

Locations ([[value.length]])

return locations[loc_type] || []; }, - _getLocationTitle: function (locations, loc_type, locationId) { + _getLocationAdminLevel: function (location) { + return location.loc_type ? location.loc_type : location.admin_level; + }, + + _getLocationTitle: function (locations, location, locationId) { + var loc_type = location.loc_type ? location.loc_type : location.admin_level; var allLocations = this._getLocations(locations, loc_type); + console.log('allLocations', allLocations); + var location = allLocations.find(function (loc) { return String(loc.id) === String(locationId); }); @@ -586,8 +601,9 @@

Locations ([[value.length]])

}, _setLocations: function (loc_type, value) { - var newLocations = Object.assign({}, this.get('locations')); + var newLocations = this.get('locations'); console.log('newLocations', newLocations); + console.log('value', value); newLocations[loc_type] = value; From 7b5c867c3a6410203ab856c2b4d0a46221705d16 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 20 May 2019 14:52:59 -0700 Subject: [PATCH 403/817] fix: Add new selected event to save multiple locations in same admin level [ch10757] --- .../indicator-locations-widget.html | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 4b8211ee9..87782c6ee 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -224,7 +224,8 @@

Locations ([[value.length]])

options="[[_getLocations(locations, item.loc_type)]]" option-value="id" option-label="title" - selected-item="{{ item.location }}" + selected-item="{{item.location}}" + on-selected-changed="_onSelectedLocationChanged" on-value-changed="_onValueChanged" data-index$="[[index]]" disabled="[[_getPending(pending, item.loc_type)]]" @@ -300,6 +301,7 @@

Locations ([[value.length]])

clusterId: Number, locations: Object, savedLocations: Object, + selectedLocations: Object, pending: Object, isPai: Boolean, searchLocationType: String, @@ -386,7 +388,40 @@

Locations ([[value.length]])

}, }, - _onValueChanged: function(event) { + _onSelectedLocationChanged: function () { + var loc_type = this.get('searchLocationType'); + var allLocations = this.get('locations')[loc_type]; + + console.log('selected id', event.detail.value); + console.log('all locations', allLocations); + + var match = allLocations.find(function (location) { + return location.id === event.detail.value; + }) + + var selectedLocations = this.get('selectedLocations'); + if (selectedLocations === undefined) { + selectedLocations = {}; + } + + if (selectedLocations[loc_type]) { + var found = selectedLocations[loc_type].find(function (location) { + return location.id === event.detail.value; + }); + + if (found !== undefined) { + return; + } + selectedLocations[loc_type].push(match); + } else { + selectedLocations[loc_type] = [match]; + }; + + this.set('selectedLocations', selectedLocations); + console.log('selected locations', selectedLocations); + }, + + _onValueChanged: function (event) { var loc_type = this.get('searchLocationType'); this.debounce('fetch-locations-' + loc_type, function() { @@ -601,7 +636,7 @@

Locations ([[value.length]])

}, _setLocations: function (loc_type, value) { - var newLocations = this.get('locations'); + var newLocations = Object.assign({}, this.get('locations')); console.log('newLocations', newLocations); console.log('value', value); From 7ac06095c03d3c958a94ae62b4c75482f4a40ed1 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Mon, 20 May 2019 18:03:50 -0700 Subject: [PATCH 404/817] feat: Attempt to fix multiple admin level locations conflicting [ch10757] --- .../indicator-locations-widget.html | 218 +++++++++++------- 1 file changed, 137 insertions(+), 81 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 87782c6ee..6150652b4 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -198,6 +198,7 @@

Locations ([[value.length]])

option-value="id" option-label="title" selected="{{item.loc_type}}" + data-index$="[[index]]" on-selected-changed="_onLocTypeChanged" required> @@ -210,7 +211,7 @@

Locations ([[value.length]])

- [[_getLocationTitle(savedLocations, item, item.id)]] + [[_getLocationTitle(savedLocations, item, item.id, index)]] @@ -221,14 +222,13 @@

Locations ([[value.length]])

@@ -388,41 +388,56 @@

Locations ([[value.length]])

}, }, - _onSelectedLocationChanged: function () { - var loc_type = this.get('searchLocationType'); - var allLocations = this.get('locations')[loc_type]; + // _onSelectedLocationChanged: function () { + // var loc_type = this.get('searchLocationType'); + // var allLocations = this.get('locations')[loc_type]; - console.log('selected id', event.detail.value); - console.log('all locations', allLocations); + // console.log('selected id', event.detail.value); + // console.log('all locations', allLocations); - var match = allLocations.find(function (location) { - return location.id === event.detail.value; - }) + // var match = allLocations.find(function (location) { + // return location.id === event.detail.value; + // }) - var selectedLocations = this.get('selectedLocations'); - if (selectedLocations === undefined) { - selectedLocations = {}; - } + // var selectedLocations = this.get('selectedLocations'); + // if (selectedLocations === undefined) { + // selectedLocations = {}; + // } + + // if (selectedLocations[loc_type]) { + // var found = selectedLocations[loc_type].find(function (location) { + // return location.id === event.detail.value; + // }); + + // if (found !== undefined) { + // return; + // } + // selectedLocations[loc_type].push(match); + // } else { + // selectedLocations[loc_type] = [match]; + // }; - if (selectedLocations[loc_type]) { - var found = selectedLocations[loc_type].find(function (location) { - return location.id === event.detail.value; - }); + // this.set('selectedLocations', selectedLocations); + // console.log('selected locations', selectedLocations); + // }, - if (found !== undefined) { + _onValueChanged: function (event) { + var loc_type = this.get('searchLocationType') || 0; + var index = -1; + + event.path.forEach(function (node) { + if (node.nodeName === 'ETOOLS-SINGLE-SELECTION-MENU') { + index = node.dataset.index; + console.log('node from forEach', node.dataset); return; } - selectedLocations[loc_type].push(match); - } else { - selectedLocations[loc_type] = [match]; - }; + }); - this.set('selectedLocations', selectedLocations); - console.log('selected locations', selectedLocations); - }, + if (index === undefined) { + return; + } - _onValueChanged: function (event) { - var loc_type = this.get('searchLocationType'); + console.log('index from onValueChanged', index); this.debounce('fetch-locations-' + loc_type, function() { var self = this; @@ -431,11 +446,11 @@

Locations ([[value.length]])

this.$$('#locations' + loc_type).thunk()() .then(function (res) { self.set('url', res.xhr.responseURL); - self._setPending(loc_type, false); - self._setLocations(loc_type, res.data.results); + self._setPending(loc_type, false, index); + self._setLocations(loc_type, res.data.results, index); }) .catch(function () { - self._setPending(loc_type, false); + self._setPending(loc_type, false, index); }); this.set('locationsInitialized', true); @@ -455,12 +470,12 @@

Locations ([[value.length]])

thunk.thunk()() .then(function(res) { - self._setPending(loc_type, false); - self._setLocations(loc_type, res.data.results); + self._setPending(loc_type, false, index); + self._setLocations(loc_type, res.data.results, index); }) .catch(function (err) { console.log(err); - self._setPending(loc_type, false); + self._setPending(loc_type, false, index); }); return; }, 1000); @@ -514,7 +529,6 @@

Locations ([[value.length]])

this.set('valueInitialized', true); this.async(function () { - console.log('VALUE', value); this.set('lockedItems', value.slice()); }); }, @@ -525,19 +539,49 @@

Locations ([[value.length]])

_add: function () { var initial = 0; + console.log('SCOPE', this); this.push('value', { loc_type: initial, }); - this._fetchLocations(initial); + var newLocations = Object.assign({}, this.get('locations')); + var value = this.get('value'); + + value.forEach(function (location, index) { + newLocations[index] = {0: []}; + }); + + this.set('locations', newLocations); + + var pendingTest = this.get('pending'); + + console.log('PENDING', pendingTest); + + var newPendingIndex = Object.keys(this.get('pending')).length; + var pending = this.get('pending'); + + pending[newPendingIndex] = { + initial: false + }; + + this.set('pending', pending); + + this._fetchLocations(initial, undefined, this.get('value').length - 1); }, _remove: function (e) { var value = this.get('value'); var toRemove = +e.target.dataset.index; + var pending = this.get('pending'); + var locations = this.get('locations'); + + delete locations[toRemove]; + delete pending[toRemove]; + var newValue = value.slice(0, toRemove).concat(value.slice(toRemove + 1)); + this.set('pending', pending); this.set('value', newValue); }, @@ -545,11 +589,30 @@

Locations ([[value.length]])

e.target.validate(); }, - _onLocTypeChanged: function (_, data) { - this._fetchLocations(data.value); + _onLocTypeChanged: function (event, data) { + console.log('event', event); + console.log('data', data); + console.log('THIS', this); + + var index = -1; + + event.path.forEach(function (node) { + if (node.nodeName === 'ETOOLS-SINGLE-SELECTION-MENU') { + index = node.dataset.index; + return; + } + }); + + if (index === -1) { + return; + } + + this._fetchLocations(data.value, undefined, index); }, - _fetchLocations: function (loc_type, title) { + _fetchLocations: function (loc_type, title, index) { + console.log('SCOPE from fetchLocations', this); + if (this.get('locations')[loc_type]) { return; } @@ -559,7 +622,7 @@

Locations ([[value.length]])

this.debounce('fetch-locations-' + loc_type, function () { var self = this; - this._setPending(loc_type, true); + this._setPending(loc_type, true, index); if (title !== undefined) { this.$$('#locations' + loc_type).params.title = title; @@ -568,57 +631,44 @@

Locations ([[value.length]])

this.$$('#locations' + loc_type).thunk()() .then(function (res) { self.set('url', res.xhr.responseURL); - self._setPending(loc_type, false); - console.log('res results', res.data.results); - self._setLocations(loc_type, res.data.results); + self._setPending(loc_type, false, index); + self._setLocations(loc_type, res.data.results, index); }) .catch(function () { - self._setPending(loc_type, false); + self._setPending(loc_type, false, index); }); }); }, _fetchInitialLocations: function (lockedItems) { - var existingLocations = {}; - - lockedItems.forEach(function (item) { - if (item.loc_type === undefined) { - if (existingLocations[item.admin_level]) { - existingLocations[item.admin_level].push(item); - } else { - existingLocations[item.admin_level] = [item]; - } - - this.set('savedLocations', existingLocations); - } else { - if (existingLocations[item.loc_type]) { - existingLocations[item.loc_type].push(item); - } else { - existingLocations[item.loc_type] = [item]; - } - - this.set('savedLocations', existingLocations); - } - }, this); + this.set('savedLocations', lockedItems); + + console.log('lockedItems in fetch initial', lockedItems); + + var newLocations = Object.assign({}, this.get('locations')); + + lockedItems.forEach(function (location, index) { + newLocations[index] = {0: []}; + }); + + this.set('locations', newLocations); }, - _getPending: function (pending, loc_type) { - return !!pending[loc_type]; + _getPending: function (pending, loc_type, index) { + return !!pending[index][loc_type]; }, - _getLocations: function (locations, loc_type) { - return locations[loc_type] || []; + _getLocations: function (locations, loc_type, index) { + return locations[index][loc_type] || []; }, _getLocationAdminLevel: function (location) { return location.loc_type ? location.loc_type : location.admin_level; }, - _getLocationTitle: function (locations, location, locationId) { + _getLocationTitle: function (locations, location, locationId, index) { var loc_type = location.loc_type ? location.loc_type : location.admin_level; - var allLocations = this._getLocations(locations, loc_type); - - console.log('allLocations', allLocations); + var allLocations = this._getLocations(locations, loc_type, index); var location = allLocations.find(function (loc) { return String(loc.id) === String(locationId); @@ -627,20 +677,26 @@

Locations ([[value.length]])

return location ? location.title : ''; }, - _setPending: function (loc_type, value) { + _setPending: function (loc_type, value, index) { var newPending = Object.assign({}, this.get('pending')); - newPending[loc_type] = value; + console.log('loc_type', loc_type); + console.log('value', value); + console.log('index', index); + + newPending[index][loc_type] = value; this.set('pending', newPending); }, - _setLocations: function (loc_type, value) { + _setLocations: function (loc_type, value, index) { var newLocations = Object.assign({}, this.get('locations')); - console.log('newLocations', newLocations); - console.log('value', value); - newLocations[loc_type] = value; + if (newLocations[index] === undefined) { + newLocations[index] = {}; + } + + newLocations[index][loc_type] = value; this.set('locations', newLocations); }, From 9a16652e07e61e9646791694e9bf09d33d6e80f1 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 09:29:10 -0700 Subject: [PATCH 405/817] feat: Verify multiple locations for same admin level is working and clean up console messages [ch10757] --- .../indicator-locations-widget.html | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 6150652b4..fe8f37146 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -301,7 +301,6 @@

Locations ([[value.length]])

clusterId: Number, locations: Object, savedLocations: Object, - selectedLocations: Object, pending: Object, isPai: Boolean, searchLocationType: String, @@ -388,39 +387,6 @@

Locations ([[value.length]])

}, }, - // _onSelectedLocationChanged: function () { - // var loc_type = this.get('searchLocationType'); - // var allLocations = this.get('locations')[loc_type]; - - // console.log('selected id', event.detail.value); - // console.log('all locations', allLocations); - - // var match = allLocations.find(function (location) { - // return location.id === event.detail.value; - // }) - - // var selectedLocations = this.get('selectedLocations'); - // if (selectedLocations === undefined) { - // selectedLocations = {}; - // } - - // if (selectedLocations[loc_type]) { - // var found = selectedLocations[loc_type].find(function (location) { - // return location.id === event.detail.value; - // }); - - // if (found !== undefined) { - // return; - // } - // selectedLocations[loc_type].push(match); - // } else { - // selectedLocations[loc_type] = [match]; - // }; - - // this.set('selectedLocations', selectedLocations); - // console.log('selected locations', selectedLocations); - // }, - _onValueChanged: function (event) { var loc_type = this.get('searchLocationType') || 0; var index = -1; @@ -428,7 +394,6 @@

Locations ([[value.length]])

event.path.forEach(function (node) { if (node.nodeName === 'ETOOLS-SINGLE-SELECTION-MENU') { index = node.dataset.index; - console.log('node from forEach', node.dataset); return; } }); @@ -437,8 +402,6 @@

Locations ([[value.length]])

return; } - console.log('index from onValueChanged', index); - this.debounce('fetch-locations-' + loc_type, function() { var self = this; @@ -539,7 +502,6 @@

Locations ([[value.length]])

_add: function () { var initial = 0; - console.log('SCOPE', this); this.push('value', { loc_type: initial, @@ -556,8 +518,6 @@

Locations ([[value.length]])

var pendingTest = this.get('pending'); - console.log('PENDING', pendingTest); - var newPendingIndex = Object.keys(this.get('pending')).length; var pending = this.get('pending'); @@ -590,10 +550,6 @@

Locations ([[value.length]])

}, _onLocTypeChanged: function (event, data) { - console.log('event', event); - console.log('data', data); - console.log('THIS', this); - var index = -1; event.path.forEach(function (node) { @@ -611,8 +567,6 @@

Locations ([[value.length]])

}, _fetchLocations: function (loc_type, title, index) { - console.log('SCOPE from fetchLocations', this); - if (this.get('locations')[loc_type]) { return; } @@ -643,8 +597,6 @@

Locations ([[value.length]])

_fetchInitialLocations: function (lockedItems) { this.set('savedLocations', lockedItems); - console.log('lockedItems in fetch initial', lockedItems); - var newLocations = Object.assign({}, this.get('locations')); lockedItems.forEach(function (location, index) { @@ -680,10 +632,6 @@

Locations ([[value.length]])

_setPending: function (loc_type, value, index) { var newPending = Object.assign({}, this.get('pending')); - console.log('loc_type', loc_type); - console.log('value', value); - console.log('index', index); - newPending[index][loc_type] = value; this.set('pending', newPending); From 51daba4c3ae9c3a9cd72596eaa726d568e70cf91 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 10:15:49 -0700 Subject: [PATCH 406/817] fix: Add conditional to check if lockedItems exist or not [ch10757] --- .../indicator-locations-widget.html | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index fe8f37146..965131869 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -599,9 +599,21 @@

Locations ([[value.length]])

var newLocations = Object.assign({}, this.get('locations')); - lockedItems.forEach(function (location, index) { - newLocations[index] = {0: []}; - }); + if (lockedItems.length > 0) { + lockedItems.forEach(function (location, index) { + if (location.admin_level === undefined) { + newLocations[index] = {}; + newLocations[index][location.loc_type] = [location]; + } else { + newLocations[index] = {}; + newLocations[index][location.admin_level] = [location]; + } + }); + } else { + lockedItems.forEach(function (location, index) { + newLocations[index] = {0: []}; + }); + } this.set('locations', newLocations); }, @@ -626,6 +638,8 @@

Locations ([[value.length]])

return String(loc.id) === String(locationId); }); + console.log('location from locationTitle', location); + return location ? location.title : ''; }, @@ -646,6 +660,8 @@

Locations ([[value.length]])

newLocations[index][loc_type] = value; + console.log('newLocations from setLocations', newLocations); + this.set('locations', newLocations); }, From afff46c5f16be0f1a2135ae75c23809088c3a421 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 10:18:00 -0700 Subject: [PATCH 407/817] fix: Send correct locations to getLocationsTitle method to render titles correctly in Edit modal [ch10757] --- .../cluster-reporting/indicator-locations-widget.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 965131869..f17d2586e 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -211,7 +211,7 @@

Locations ([[value.length]])

- [[_getLocationTitle(savedLocations, item, item.id, index)]] + [[_getLocationTitle(locations, item, item.id, index)]] @@ -638,8 +638,6 @@

Locations ([[value.length]])

return String(loc.id) === String(locationId); }); - console.log('location from locationTitle', location); - return location ? location.title : ''; }, From 383a09a10eeb3ed7e4f4b29d0771f32b2c4427fb Mon Sep 17 00:00:00 2001 From: Silin Na Date: Tue, 21 May 2019 12:35:51 -0700 Subject: [PATCH 408/817] feat: Created a utility function to clone CA indicator to PP [ch10764] + create_reportable_for_pp_from_ca_reportable + Added new constant for EXTERNAL_DATA_SOURCES --- django_api/django_api/apps/core/common.py | 1 + .../django_api/apps/indicator/models.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/django_api/django_api/apps/core/common.py b/django_api/django_api/apps/core/common.py index a94c71ddb..fff6acdb6 100644 --- a/django_api/django_api/apps/core/common.py +++ b/django_api/django_api/apps/core/common.py @@ -207,6 +207,7 @@ EXTERNAL_DATA_SOURCES = Choices( ("HPC", "HPC"), ("OPS", "OPS"), + ("UNICEF", "UNICEF"), ) CURRENCIES = Choices( diff --git a/django_api/django_api/apps/indicator/models.py b/django_api/django_api/apps/indicator/models.py index 93a843bac..6d2d9465c 100644 --- a/django_api/django_api/apps/indicator/models.py +++ b/django_api/django_api/apps/indicator/models.py @@ -439,6 +439,30 @@ def create_reportable_for_pa_from_ca_reportable(pa, ca_reportable): pa_reportable.disaggregations.add(*ca_reportable.disaggregations.all()) +def create_reportable_for_pp_from_ca_reportable(pp, ca_reportable): + """ + Copies one CA reportable instance to a partner activity. + + Arguments: + pp {partner.models.PartnerProject} -- PartnerProject to copy to + reportable {indicator.models.Reportable} -- ClusterActivity Reportable + + Raises: + ValidationError -- Django Exception + """ + + reportable_data_to_sync = get_reportable_data_to_clone(ca_reportable) + reportable_data_to_sync['total'] = dict([('c', 0), ('d', 1), ('v', 0)]) + reportable_data_to_sync["content_object"] = pp + reportable_data_to_sync["blueprint"] = ca_reportable.blueprint + reportable_data_to_sync["parent_indicator"] = ca_reportable + pp_reportable = Reportable.objects.create(**reportable_data_to_sync) + + pp_reportable.disaggregations.add(*ca_reportable.disaggregations.all()) + + return pp_reportable + + def create_reportable_for_pp_from_co_reportable(pp, co_reportable): """ Copies one CO reportable instance to a partner project. From 5c36b72508e259484e3dd5886737bf4f438c6f5f Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 12:36:28 -0700 Subject: [PATCH 409/817] fix: Preventing adding new location from clearing out other location value [ch10757] --- .../indicator-locations-widget.html | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index f17d2586e..d060113e7 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -227,6 +227,7 @@

Locations ([[value.length]])

option-label="title" selected-item="{{item.location}}" on-value-changed="_onValueChanged" + on-selected-changed="_onSelectedChanged" data-index$="[[index]]" disabled="[[_getPending(pending, item.loc_type, index)]]" required> @@ -387,10 +388,18 @@

Locations ([[value.length]])

}, }, + _onSelectedChanged: function (event) { + console.log('selected!', this.value); + }, + _onValueChanged: function (event) { var loc_type = this.get('searchLocationType') || 0; var index = -1; + if (event.detail.value === '') { + return; + } + event.path.forEach(function (node) { if (node.nodeName === 'ETOOLS-SINGLE-SELECTION-MENU') { index = node.dataset.index; @@ -398,10 +407,12 @@

Locations ([[value.length]])

} }); - if (index === undefined) { - return; + if (loc_type === 0 && index === undefined) { + index = 0; } + var setLocations = this.get('locations'); + this.debounce('fetch-locations-' + loc_type, function() { var self = this; @@ -507,11 +518,22 @@

Locations ([[value.length]])

loc_type: initial, }); - var newLocations = Object.assign({}, this.get('locations')); + this.set('searchLocationType', 0); + this.set('locationsInitialized', true); + + var newLocations = this.get('locations'); var value = this.get('value'); + console.log('value on add', value); + console.log('locations on add', newLocations); + // if (newLocations[0] !== undefined) { + // return; + // } + value.forEach(function (location, index) { - newLocations[index] = {0: []}; + if (location.location === undefined) { + newLocations[index] = {0: []}; + } }); this.set('locations', newLocations); @@ -567,9 +589,9 @@

Locations ([[value.length]])

}, _fetchLocations: function (loc_type, title, index) { - if (this.get('locations')[loc_type]) { - return; - } + // if (this.get('locations')[loc_type]) { + // return; + // } this.set('searchLocationType', loc_type); From 20e3cadccb3e0f58a4e4f2821a2a90e542ee2890 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 13:14:21 -0700 Subject: [PATCH 410/817] fix: Add necessary conditionals to Edit Indicator component [ch10757] --- .../cluster-reporting/indicator-editing-modal.html | 13 +++++++++++++ .../indicator-locations-widget.html | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html index 1c7a04e11..cdb720d2b 100644 --- a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html @@ -547,6 +547,19 @@

Edit Indicator

_save: function () { var self = this; + var wholeData = this.get('data'); + wholeData.locations.forEach(function (location) { + if (location.location.id !== undefined) { + var id = location.location.id; + var name = location.location.title; + location.location = id; + location.title = name; + } + }); + + console.log('wholeData', wholeData); + console.log('this data', this.get('data')); + if (!this._fieldsAreValid()) { return; } diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index d060113e7..0323e90e5 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -666,6 +666,16 @@

Locations ([[value.length]])

_setPending: function (loc_type, value, index) { var newPending = Object.assign({}, this.get('pending')); + console.log('loc_type', loc_type); + console.log('value', value); + console.log('index', index); + + console.log('newPending', newPending); + + if (newPending[index] === undefined) { + newPending[index] = {}; + } + newPending[index][loc_type] = value; this.set('pending', newPending); From 7f877ebab4c52dff4b0682f44b0e016b20746f99 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 13:33:59 -0700 Subject: [PATCH 411/817] fix: Add conditional to check for existing location and clean up console messages [ch10757] --- .../indicator-editing-modal.html | 3 --- .../indicator-locations-widget.html | 20 +------------------ 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html index cdb720d2b..6afeb3455 100644 --- a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html @@ -557,9 +557,6 @@

Edit Indicator

} }); - console.log('wholeData', wholeData); - console.log('this data', this.get('data')); - if (!this._fieldsAreValid()) { return; } diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 0323e90e5..7dcab21c7 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -388,10 +388,6 @@

Locations ([[value.length]])

}, }, - _onSelectedChanged: function (event) { - console.log('selected!', this.value); - }, - _onValueChanged: function (event) { var loc_type = this.get('searchLocationType') || 0; var index = -1; @@ -524,14 +520,8 @@

Locations ([[value.length]])

var newLocations = this.get('locations'); var value = this.get('value'); - console.log('value on add', value); - console.log('locations on add', newLocations); - // if (newLocations[0] !== undefined) { - // return; - // } - value.forEach(function (location, index) { - if (location.location === undefined) { + if (location.location === undefined && newLocations[index] === undefined) { newLocations[index] = {0: []}; } }); @@ -666,12 +656,6 @@

Locations ([[value.length]])

_setPending: function (loc_type, value, index) { var newPending = Object.assign({}, this.get('pending')); - console.log('loc_type', loc_type); - console.log('value', value); - console.log('index', index); - - console.log('newPending', newPending); - if (newPending[index] === undefined) { newPending[index] = {}; } @@ -690,8 +674,6 @@

Locations ([[value.length]])

newLocations[index][loc_type] = value; - console.log('newLocations from setLocations', newLocations); - this.set('locations', newLocations); }, From 5de1048b599dd35819143a06415ada67f420f863 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 13:50:10 -0700 Subject: [PATCH 412/817] fix: Properly restructure locations object for Edit Project modal and clean up unnecessary code [ch10757] --- .../cluster-reporting/indicator-locations-widget.html | 1 - .../planned-action/projects/creation-modal.html | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 7dcab21c7..8ebf0a07a 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -227,7 +227,6 @@

Locations ([[value.length]])

option-label="title" selected-item="{{item.location}}" on-value-changed="_onValueChanged" - on-selected-changed="_onSelectedChanged" data-index$="[[index]]" disabled="[[_getPending(pending, item.loc_type, index)]]" required> diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index ff3ba132a..c5e075aa7 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -1119,9 +1119,14 @@

Fields required

_save: function () { var wholeData = this.get('data'); + wholeData.locations = this.data.locations; var newLocations = wholeData.locations.map(function (location) { - return location.location; + if (location.location !== undefined) { + return location.location; + } else { + return location; + } }); wholeData.locations = newLocations; // Un-nest location data from indicator-locations-widget From 1e16ed5448e2d993432b6609d227aa64daea0860 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 14:13:56 -0700 Subject: [PATCH 413/817] fix: Delete location titles on Edit Indicator to successfuly PUT and add description field to Add Project modal defaults to properly catch empty description [ch10757] --- .../indicator-editing-modal.html | 5 ++-- .../projects/creation-modal.html | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html index 6afeb3455..7a747462d 100644 --- a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html @@ -549,11 +549,12 @@

Edit Indicator

var wholeData = this.get('data'); wholeData.locations.forEach(function (location) { + if (location.title !== undefined) { + delete location.title; + } if (location.location.id !== undefined) { var id = location.location.id; - var name = location.location.title; location.location = id; - location.title = name; } }); diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index c5e075aa7..9285c9e96 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -433,6 +433,16 @@

Fields required

+ + + Fields required on-input="_validate"> - - - Fields required this.data = { 'custom_fields': [], 'funding': {}, - 'locations': [] + 'locations': [], + 'description': '', }; this.selectedClusters = []; } @@ -1110,14 +1111,14 @@

Fields required

this.set('data', { 'custom_fields': [], 'funding': {}, - 'locations': [] + 'locations': [], + 'description': '', }); this.set('projectDetails', {}); this.set('errors', {}); }, _save: function () { - var wholeData = this.get('data'); wholeData.locations = this.data.locations; From 7cacf4255e4903a49f8bcb00335c3b7df74cc3b2 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Tue, 21 May 2019 16:26:05 -0700 Subject: [PATCH 414/817] feat: Add new labels for localization (up to top of report detail page) [ch11929] --- polymer/src/locales.json | 90 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/polymer/src/locales.json b/polymer/src/locales.json index 6a18f1392..a0971180b 100644 --- a/polymer/src/locales.json +++ b/polymer/src/locales.json @@ -18,7 +18,95 @@ "progress_reports": "Progress Reports", "indicators": "Indicators", "id_management": "ID Management", - "knowledge_base": "Knowledge Base" + "knowledge_base": "Knowledge Base", + "partner_details": "Partner Details", + "full_name": "Full Name", + "short_name": "Short Name", + "alternate_name": "Alternate Name", + "vendor_number": "Vendor Number", + "partner_type": "Partner Type", + "shared_partners": "Shared Partner(s)", + "date_last_assessed": "Date Last Assessed Against Core Values", + "address": "Address", + "phone_number": "Phone Number", + "email_address": "E-mail Address", + "financial_management": "Financial Management Capacity Assessment", + "partner_risk": "Partner Risk Rating", + "date_assessment": "Date of Assessment", + "basis_risk": "Basis For Risk Rating", + "pd_ref_and_title": "PD/SSFA ToR ref. # and Title", + "pd_ssfa_status": "PD/SSFA status", + "location": "Location", + "clear": "Clear", + "list_pds": "List of PDs", + "results_to_show": "results to show", + "pd_ref_number": "PD/SSFA ToR ref. #", + "start_date": "Start date", + "end_date": "End date", + "cso_contribution": "CSO contribution", + "unicef_cash": "UNICEF cash", + "unicef_supplies": "UNICEF supplies", + "planned_budget": "Planned Budget", + "cash_transfers": "Cash Transfers to Date (%)", + "signed": "Signed", + "active": "Active", + "suspended": "Suspended", + "ended": "Ended", + "closed": "Closed", + "terminated": "Terminated", + "reports": "Reports", + "all": "All", + "overdue": "Overdue", + "sent_back": "Sent back", + "due": "Due", + "submitted": "Submitted", + "accepted": "Accepted", + "report_type": "Report Type", + "qpr": "QPR (Quarterly Progress Report)", + "hr": "HR (Humanitarian Report)", + "sr": "SR (Special Report)", + "report_number": "Report #", + "due_date": "Due date", + "date_of_submission": "Date of submission", + "reporting_period": "Reporting period", + "list_of_reports": "List of Reports", + "pd_status": "PD status", + "indicator_title": "Indicator Title", + "list_of_indicators": "List of Indicators", + "indicator": "Indicator", + "calc_across_locations": "Calculation (across locations)", + "calc_across_periods": "Calculation (across reporting periods)", + "baseline": "Baseline", + "target": "Target", + "achieved": "Achieved", + "current_progress": "Current progress", + "against_target": "Against Target", + "details": "Details", + "calculation_methods": "Calculation Methods", + "agreement": "Agreement", + "document_type": "Document Type", + "reference_number": "Reference Number", + "title": "Title", + "unicef_offices": "UNICEF Office(s)", + "unicef_points": "UNICEF Focal Point(s)", + "partner_points": "Partner Focal Point(s)", + "pd_ssfa_document": "PD/SSFA Document", + "total_budget": "Total Budget", + "disbursements": "$ disbursements to date (%)", + "locations": "Location(s)", + "amendments": "Amendments", + "amendment_types": "Amendment Types", + "signed_date": "Signed Date", + "reporting_requirements": "Reporting Requirements", + "status": "Status", + "enter_data": "Enter data for", + "reporting_on_results": "Reporting on Results", + "other_info": "Other Info", + "pd_output": "PD output", + "show_incomplete_only": "Show incomplete only", + "refresh": "Refresh", + "generate_uploader": "Generate Uploader Template", + "import_template": "Import Template" }, "fr": { "ip_reporting": "Reporting Partenaires UNICEF", From 637d5491f9742b04b3ce8e5cfa27f0b8ae19630e Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 22 May 2019 11:03:37 -0700 Subject: [PATCH 415/817] feat: Created Through model for PartnerActivity [ch10764] + PartnerActivityProjectContext + DB migraitons + Django admin fixes for Partner Activity --- .../migrations/0003_auto_20190522_1759.py | 30 ++++++++ .../migrations/0005_auto_20190522_1759.py | 35 +++++++++ .../migrations/0004_auto_20190522_1759.py | 35 +++++++++ django_api/django_api/apps/partner/admin.py | 24 ++++-- .../migrations/0004_auto_20190522_1759.py | 74 +++++++++++++++++++ django_api/django_api/apps/partner/models.py | 29 +++++--- 6 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 django_api/django_api/apps/cluster/migrations/0003_auto_20190522_1759.py create mode 100644 django_api/django_api/apps/core/migrations/0005_auto_20190522_1759.py create mode 100644 django_api/django_api/apps/indicator/migrations/0004_auto_20190522_1759.py create mode 100644 django_api/django_api/apps/partner/migrations/0004_auto_20190522_1759.py diff --git a/django_api/django_api/apps/cluster/migrations/0003_auto_20190522_1759.py b/django_api/django_api/apps/cluster/migrations/0003_auto_20190522_1759.py new file mode 100644 index 000000000..36738ccf0 --- /dev/null +++ b/django_api/django_api/apps/cluster/migrations/0003_auto_20190522_1759.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-22 17:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cluster', '0002_auto_20180711_2325'), + ] + + operations = [ + migrations.AlterField( + model_name='cluster', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='clusteractivity', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='clusterobjective', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + ] diff --git a/django_api/django_api/apps/core/migrations/0005_auto_20190522_1759.py b/django_api/django_api/apps/core/migrations/0005_auto_20190522_1759.py new file mode 100644 index 000000000..e38514598 --- /dev/null +++ b/django_api/django_api/apps/core/migrations/0005_auto_20190522_1759.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-22 17:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_auto_20181024_0034'), + ] + + operations = [ + migrations.AlterField( + model_name='location', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='prprole', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='responseplan', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='workspace', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + ] diff --git a/django_api/django_api/apps/indicator/migrations/0004_auto_20190522_1759.py b/django_api/django_api/apps/indicator/migrations/0004_auto_20190522_1759.py new file mode 100644 index 000000000..c0ad2b0d8 --- /dev/null +++ b/django_api/django_api/apps/indicator/migrations/0004_auto_20190522_1759.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-22 17:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('indicator', '0003_auto_20190507_1908'), + ] + + operations = [ + migrations.AlterField( + model_name='disaggregation', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='disaggregationvalue', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='indicatorblueprint', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='reportable', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + ] diff --git a/django_api/django_api/apps/partner/admin.py b/django_api/django_api/apps/partner/admin.py index 49716217a..145ac75c2 100644 --- a/django_api/django_api/apps/partner/admin.py +++ b/django_api/django_api/apps/partner/admin.py @@ -4,7 +4,9 @@ Partner, PartnerProject, PartnerActivity, - PartnerProjectFunding) + PartnerProjectFunding, + PartnerActivityProjectContext, +) class PartnerAdmin(admin.ModelAdmin): @@ -14,17 +16,23 @@ class PartnerAdmin(admin.ModelAdmin): search_fields = ('title', 'short_title', 'alternate_title') +class PartnerActivityProjectContextInline(admin.StackedInline): + model = PartnerActivityProjectContext + extra = 1 + min_num = 0 + + class PartnerProjectAdmin(admin.ModelAdmin): - list_display = ('title', 'partner', 'description', 'start_date', - 'end_date', 'status',) - list_filter = ('status', 'partner', 'clusters') - search_fields = ('title', 'description', 'additional_information') + list_display = ('title', 'partner', 'description',) + list_filter = ('status', 'partner', 'clusters',) + search_fields = ('title', 'description', 'additional_information',) class PartnerActivityAdmin(admin.ModelAdmin): - list_display = ('title', 'partner', 'project', 'cluster_activity', - 'cluster_objective', 'start_date', 'end_date', 'status',) - list_filter = ('status', 'partner', 'project') + inlines = (PartnerActivityProjectContextInline, ) + list_display = ('title', 'partner', 'cluster_activity', + 'cluster_objective',) + list_filter = ('partner',) search_fields = ('title', ) diff --git a/django_api/django_api/apps/partner/migrations/0004_auto_20190522_1759.py b/django_api/django_api/apps/partner/migrations/0004_auto_20190522_1759.py new file mode 100644 index 000000000..9d3735f96 --- /dev/null +++ b/django_api/django_api/apps/partner/migrations/0004_auto_20190522_1759.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-22 17:59 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partner', '0003_auto_20181024_0032'), + ] + + operations = [ + migrations.CreateModel( + name='PartnerActivityProjectContext', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('status', models.CharField(choices=[('Ong', 'Ongoing'), ('Pla', 'Planned'), ('Com', 'Completed')], default='Ong', max_length=3)), + ], + ), + migrations.RemoveField( + model_name='partneractivity', + name='end_date', + ), + migrations.RemoveField( + model_name='partneractivity', + name='project', + ), + migrations.RemoveField( + model_name='partneractivity', + name='start_date', + ), + migrations.RemoveField( + model_name='partneractivity', + name='status', + ), + migrations.AlterField( + model_name='partner', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AlterField( + model_name='partnerproject', + name='external_source', + field=models.TextField(blank=True, choices=[('HPC', 'HPC'), ('OPS', 'OPS'), ('UNICEF', 'UNICEF')], null=True), + ), + migrations.AddField( + model_name='partneractivityprojectcontext', + name='activity', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='partner.PartnerActivity'), + ), + migrations.AddField( + model_name='partneractivityprojectcontext', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='partner.PartnerProject'), + ), + migrations.AddField( + model_name='partneractivity', + name='projects', + field=models.ManyToManyField(related_name='partner_activities', through='partner.PartnerActivityProjectContext', to='partner.PartnerProject'), + ), + migrations.AlterUniqueTogether( + name='partneractivityprojectcontext', + unique_together=set([('project', 'activity')]), + ), + ] diff --git a/django_api/django_api/apps/partner/models.py b/django_api/django_api/apps/partner/models.py index a155f518c..e53355b9f 100644 --- a/django_api/django_api/apps/partner/models.py +++ b/django_api/django_api/apps/partner/models.py @@ -311,14 +311,17 @@ class PartnerActivity(TimeStampedModel): partner is allowed to define their ideas that wasn't defined. related models: - partner.PartnerProject (ForeignKey): "project" + partner.PartnerProject (ManyToMany): "projects" partner.Partner (ForeignKey): "partner" cluster.ClusterActivity (ForeignKey): "cluster_activity" indicator.Reportable (GenericRelation): "reportables" """ title = models.CharField(max_length=2048) - project = models.ForeignKey(PartnerProject, null=True, - related_name="partner_activities") + projects = models.ManyToManyField( + PartnerProject, + related_name="partner_activities", + through="PartnerActivityProjectContext", + ) partner = models.ForeignKey(Partner, related_name="partner_activities") cluster_activity = models.ForeignKey('cluster.ClusterActivity', related_name="partner_activities", @@ -330,12 +333,6 @@ class PartnerActivity(TimeStampedModel): related_query_name='partner_activities') locations = models.ManyToManyField('core.Location', related_name="partner_activities") - start_date = models.DateField() - end_date = models.DateField() - - # PartnerActivity shares the status flags with PartnerProject - status = models.CharField(max_length=3, choices=PARTNER_PROJECT_STATUS, - default=PARTNER_PROJECT_STATUS.ongoing) class Meta: ordering = ['-id'] @@ -366,3 +363,17 @@ def check_pa_double_fks(sender, instance, **kwargs): raise Exception( "PartnerActivity cannot belong to both ClusterActivity and ClusterObjective" ) + + +class PartnerActivityProjectContext(TimeStampedModel): + project = models.ForeignKey("PartnerProject", on_delete=models.CASCADE) + activity = models.ForeignKey("PartnerActivity", on_delete=models.CASCADE) + start_date = models.DateField() + end_date = models.DateField() + + # PartnerActivity shares the status flags with PartnerProject + status = models.CharField(max_length=3, choices=PARTNER_PROJECT_STATUS, + default=PARTNER_PROJECT_STATUS.ongoing) + + class Meta: + unique_together = ('project', 'activity') From ef4605751b65dc6dd685e3bf529369e43e5ca0cf Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 13:09:37 -0700 Subject: [PATCH 416/817] fix: Add conditionals to catch loc_type difference before mutating objects [ch10757] --- .../projects/creation-modal.html | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index 9285c9e96..c027c83f5 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -1119,18 +1119,27 @@

Fields required

}, _save: function () { - var wholeData = this.get('data'); - wholeData.locations = this.data.locations; + var rawLocations = this.get('data.locations'); - var newLocations = wholeData.locations.map(function (location) { - if (location.location !== undefined) { - return location.location; + var locTypeCheck = rawLocations.every(function (location) { + if (rawLocations[0].loc_type === undefined) { + return location.loc_type || location.admin_level == rawLocations[0].admin_level; } else { - return location; + return location.loc_type == rawLocations[0].loc_type; } }); - wholeData.locations = newLocations; // Un-nest location data from indicator-locations-widget + if (locTypeCheck === true) { + var changedLocations = rawLocations.map(function (location) { + if (location.location !== undefined) { + return location.location; + } else { + return location; + } + }); + + this.data.locations = changedLocations; + } var self = this; if (!this.data.partner_id) { From 9dfb83c4e04258dd32fa643074691d1bc2c07672 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 13:52:48 -0700 Subject: [PATCH 417/817] fix: Revert conditional back to original state and simply reset locations on error [ch10757] --- .../indicator-locations-widget.html | 6 +++--- .../projects/creation-modal.html | 21 ++++++------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 8ebf0a07a..cbddc3aa6 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -578,9 +578,9 @@

Locations ([[value.length]])

}, _fetchLocations: function (loc_type, title, index) { - // if (this.get('locations')[loc_type]) { - // return; - // } + if (loc_type === undefined) { + return; + } this.set('searchLocationType', loc_type); diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index c027c83f5..dd0b3ab28 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -1121,25 +1121,15 @@

Fields required

_save: function () { var rawLocations = this.get('data.locations'); - var locTypeCheck = rawLocations.every(function (location) { - if (rawLocations[0].loc_type === undefined) { - return location.loc_type || location.admin_level == rawLocations[0].admin_level; + var changedLocations = rawLocations.map(function (location) { + if (location.location !== undefined) { + return location.location; } else { - return location.loc_type == rawLocations[0].loc_type; + return location; } }); - if (locTypeCheck === true) { - var changedLocations = rawLocations.map(function (location) { - if (location.location !== undefined) { - return location.location; - } else { - return location; - } - }); - - this.data.locations = changedLocations; - } + this.set('data.locations', changedLocations); var self = this; if (!this.data.partner_id) { @@ -1165,6 +1155,7 @@

Fields required

}) .catch(function (err) { // jshint ignore:line self.updatePending = false; + self.set('data.locations', rawLocations); self.set('errors', err.data); }); }, From b861fe83ad93d26fcc93f7d5cf0934f28dcfb2e2 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 22 May 2019 14:27:38 -0700 Subject: [PATCH 418/817] refactor: Added partner project creation in PD sync [ch10764] + PartnerProject lookup by workspace business area code and PD ID + PartnerActivityProjectContext model creation after PartnerActivity auto-adoption + Clone CA Reportable to create PartnerProject Reportable --- django_api/django_api/apps/unicef/tasks.py | 55 ++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/django_api/django_api/apps/unicef/tasks.py b/django_api/django_api/apps/unicef/tasks.py index 33bc57bdf..c3eb459cf 100644 --- a/django_api/django_api/apps/unicef/tasks.py +++ b/django_api/django_api/apps/unicef/tasks.py @@ -12,7 +12,7 @@ from core.api import PMP_API from core.models import Workspace, GatewayType, Location, PRPRole from core.serializers import PMPGatewayTypeSerializer, PMPLocationSerializer -from core.common import PARTNER_ACTIVITY_STATUS, PRP_ROLE_TYPES +from core.common import PARTNER_ACTIVITY_STATUS, PRP_ROLE_TYPES, EXTERNAL_DATA_SOURCES from partner.models import PartnerActivity @@ -37,9 +37,10 @@ DisaggregationValue, ReportableLocationGoal, create_pa_reportables_from_ca, + create_reportable_for_pp_from_ca_reportable, ) -from partner.models import Partner +from partner.models import Partner, PartnerProject, PartnerActivityProjectContext logger = logging.getLogger(__name__) @@ -334,6 +335,7 @@ def process_programme_documents(fast=False, area=False): # Create LLO d['cp_output'] = pdresultlink.id d['external_business_area_code'] = workspace.business_area_code + llo = process_model( LowerLevelOutput, PMPLLOSerializer, d, { @@ -499,22 +501,52 @@ def process_programme_documents(fast=False, area=False): cai = Reportable.objects.get(id=int(i['cluster_indicator_id'])) reportable.ca_indicator_used_by_reporting_entity = cai + # Partner Project for this PD check + if not PartnerProject.objects.filter( + external_id="{}/{}".format(workspace.business_area_code, pd.id), + external_source=EXTERNAL_DATA_SOURCES.UNICEF + ).exists(): + pp = PartnerProject.objects.create( + external_id="{}/{}".format(workspace.business_area_code, pd.id), + external_source=EXTERNAL_DATA_SOURCES.UNICEF, + title=item['title'], + partner=partner, + ) + + logger.info( + "Created a new PartnerProject " + "from PD: " + str(item['reference_number']) + ) + else: + pp = PartnerProject.objects.get( + external_id="{}/{}".format(area, item['id']), + external_source=EXTERNAL_DATA_SOURCES.UNICEF + ) + # Force adoption of PartnerActivity from ClusterActivity Indicator if pd.partner.id not in cai.content_object.partner_activities.values_list( 'partner', flat=True): try: partner_activity = PartnerActivity.objects.create( title=cai.blueprint.title, - project=pd.partner.partner_projects.first(), + project=pp, partner=pd.partner, cluster_activity=cai.content_object, start_date=cai.content_object.response_plan.start, end_date=cai.content_object.response_plan.end, status=PARTNER_ACTIVITY_STATUS.ongoing, ) + + PartnerActivityProjectContext.objects.create( + project=pp, + activity=partner_activity, + start_date=item['start_date'], + end_date=item['end_date'], + ) + except Exception as e: logger.exception( - "Cannot force adopt PartnerActivity from ClusterActivity " + "Cannot force adopt PartnerActivity and its project context from ClusterActivity " "for dual reporting - skipping link!: " + str(e) ) continue @@ -535,6 +567,21 @@ def process_programme_documents(fast=False, area=False): partner_activity.delete() continue + try: + # Grab Cluster Activity instance from + # this newly created Partner Activity instance + create_reportable_for_pp_from_ca_reportable( + pp, cai + ) + except Exception as e: + logger.exception( + "Cannot create Reportables for PD Partner Project " + "from referenced Cluster Activity Reportable " + " - skipping link!: " + str(e) + ) + + continue + except Reportable.DoesNotExist: logger.exception( "No ClusterActivity Reportable found " From 9a543c4d7175118bf6e1c4331b582b9a5ea63ea6 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 15:06:34 -0700 Subject: [PATCH 419/817] fix: Add conditional for displaying error when no location is set and clean up unnecessary code [ch10757] --- .../indicator-locations-widget.html | 20 ------------------- .../projects/creation-modal.html | 17 +++++++++++++++- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index cbddc3aa6..28c7b5c2e 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -406,29 +406,9 @@

Locations ([[value.length]])

index = 0; } - var setLocations = this.get('locations'); - this.debounce('fetch-locations-' + loc_type, function() { var self = this; - if (this.locationsInitialized === false) { - this.$$('#locations' + loc_type).thunk()() - .then(function (res) { - self.set('url', res.xhr.responseURL); - self._setPending(loc_type, false, index); - self._setLocations(loc_type, res.data.results, index); - }) - .catch(function () { - self._setPending(loc_type, false, index); - }); - - this.set('locationsInitialized', true); - } - - if (event.detail.value === '') { - return; - } - var thunk = self.$.search; thunk.url = self.get('url'); diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index dd0b3ab28..3ce2d411b 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -1119,7 +1119,10 @@

Fields required

}, _save: function () { + var self = this; + var rawLocations = this.get('data.locations'); + console.log('raw locations', rawLocations); var changedLocations = rawLocations.map(function (location) { if (location.location !== undefined) { @@ -1129,9 +1132,21 @@

Fields required

} }); + var noLocationSet = false; + + changedLocations.forEach(function (location) { + if (location.title === undefined) { + self.set('errors', 'no location set - please set a location.'); + noLocationSet = true; + } + }); + + if (noLocationSet === true) { + return; + } + this.set('data.locations', changedLocations); - var self = this; if (!this.data.partner_id) { this.data.partner_id = this.partnerID; } From d3d0bd19d35a9ed53611fb18c03f2ac52c3490a3 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 15:42:21 -0700 Subject: [PATCH 420/817] fix: Have Edit Indicator modal correctly maintain title on mismatched admin level [ch10757] --- .../indicator-editing-modal.html | 26 ++++++++++++++----- .../projects/creation-modal.html | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html index 7a747462d..b5fcee40d 100644 --- a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html @@ -547,21 +547,34 @@

Edit Indicator

_save: function () { var self = this; - var wholeData = this.get('data'); - wholeData.locations.forEach(function (location) { - if (location.title !== undefined) { - delete location.title; - } - if (location.location.id !== undefined) { + var noLocationSet = false; + var rawLocations = this.get('data.locations'); + + var changedLocations = rawLocations.map(function (location) { + if (location.location !== undefined && location.location.id !== undefined) { var id = location.location.id; + var title = location.location.title; location.location = id; + return location; + } else if (location.loc_type !== undefined && location.location === undefined) { + self.set('errors', 'No location set - please set a location.'); + noLocationSet = true; + return location; + } else { + return location; } }); + if (noLocationSet === true) { + return; + } + if (!this._fieldsAreValid()) { return; } + this.set('data.locations', changedLocations); + this.set('updatePending', true); if (this.data.frequency !== 'Csd') { @@ -579,6 +592,7 @@

Edit Indicator

}) .catch(function (err) { self.set('errors', err.data); + self.set('data.locations', rawLocations); self.set('updatePending', false); }); }, diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index 3ce2d411b..afd0c6d68 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -1136,7 +1136,7 @@

Fields required

changedLocations.forEach(function (location) { if (location.title === undefined) { - self.set('errors', 'no location set - please set a location.'); + self.set('errors', 'No location set - please set a location.'); noLocationSet = true; } }); From 10c09d9163f2d282b6861f1933f6694b0979590c Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 15:54:57 -0700 Subject: [PATCH 421/817] fix: Have Add Indicator modal correctly maintain locations on 400 errors [ch10757] --- .../cluster-reporting/indicator-modal.html | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index e530fedd4..ce8c79471 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -932,18 +932,35 @@

[[modalTitle]]

_save: function () { var self = this; - var wholeData = this.get('data'); - wholeData.locations.forEach(function (location) { - var id = location.location.id; - var title = location.location.title; - location.location = id; - location.title = title; + var noLocationSet = false; + var rawLocations = this.get('data.locations'); + + var changedLocations = rawLocations.map(function (location) { + if (location.location !== undefined && location.location.id !== undefined) { + var id = location.location.id; + var title = location.location.title; + location.location = id; + location.title = title; + return location; + } else if (location.loc_type !== undefined && location.location === undefined) { + self.set('errors', 'No location set - please set a location.'); + noLocationSet = true; + return location; + } else { + return location; + } }); + if (noLocationSet === true) { + return; + } + if (!this._fieldsAreValid()) { return; } + this.set('data.locations', changedLocations); + if (this.mode === 'objectives') { this._setDisaggregations(); @@ -1000,6 +1017,7 @@

[[modalTitle]]

}) .catch(function (err) { self.set('errors', err.data); + self.set('data.locations', rawLocations); self.set('updatePending', false); }); } else { @@ -1007,6 +1025,8 @@

[[modalTitle]]

this._setDisaggregations(); this.$.indicators.body = this._processData(this.data); + console.log('body data', this.$.indicators.body); + this.$.indicators.thunk()() .then(function (res) { self.fire('indicator-added', res.data); @@ -1016,6 +1036,7 @@

[[modalTitle]]

}) .catch(function (err) { self.set('errors', err.data); + self.set('data.locations', rawLocations); self.set('updatePending', false); }); } From fbf0c27940909d06dc6dc6ef87342238e0bb1dc7 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 15:55:51 -0700 Subject: [PATCH 422/817] fix: Clean up console message from indicator-modal component [ch10757] --- polymer/src/elements/cluster-reporting/indicator-modal.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index ce8c79471..654fc459c 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -1025,8 +1025,6 @@

[[modalTitle]]

this._setDisaggregations(); this.$.indicators.body = this._processData(this.data); - console.log('body data', this.$.indicators.body); - this.$.indicators.thunk()() .then(function (res) { self.fire('indicator-added', res.data); From dfd6a45e57ad6183f2be31d38ec9ef0fb3fa61e7 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Wed, 22 May 2019 16:27:52 -0700 Subject: [PATCH 423/817] refactor: Fix any partner activity's project reference to use M2M field [ch10764] + Export indicators + GET API serializers + Model properties --- .../apps/cluster/export_indicators.py | 2 +- .../django_api/apps/indicator/serializers.py | 17 +++++++++-------- django_api/django_api/apps/indicator/views.py | 2 +- django_api/django_api/apps/partner/models.py | 5 +++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/django_api/django_api/apps/cluster/export_indicators.py b/django_api/django_api/apps/cluster/export_indicators.py index 8ee8b31b3..3907f10cf 100644 --- a/django_api/django_api/apps/cluster/export_indicators.py +++ b/django_api/django_api/apps/cluster/export_indicators.py @@ -143,7 +143,7 @@ def fill_sheet(self, disaggregation_types, indicators): partner_activity.cluster_activity else partner_activity.cluster_objective.cluster cluster_objective = partner_activity.cluster_activity.cluster_objective if \ partner_activity.cluster_activity else partner_activity.cluster_objective - partner_project = partner_activity.project + partner_project = partner_activity.projects.first().project elif partner_project: cluster = partner_project.clusters.first() cluster_objective = cluster.cluster_objectives.first() diff --git a/django_api/django_api/apps/indicator/serializers.py b/django_api/django_api/apps/indicator/serializers.py index 8f16c7089..acf01eeab 100644 --- a/django_api/django_api/apps/indicator/serializers.py +++ b/django_api/django_api/apps/indicator/serializers.py @@ -1758,10 +1758,11 @@ def get_project(self, obj): if isinstance(obj.reportable.content_object, (PartnerProject, )): return {"id": obj.reportable.content_object.id, "title": obj.reportable.content_object.title} elif isinstance(obj.reportable.content_object, (PartnerActivity, )): - if obj.reportable.content_object.project: + if obj.reportable.content_object.projects.exists(): + project = obj.reportable.content_object.projects.first() return { - "id": obj.reportable.content_object.project.id, - "title": obj.reportable.content_object.project.title + "id": project.id, + "title": project.title } else: return None @@ -1933,11 +1934,11 @@ def get_indicator_reports(self, obj): return [] def get_project(self, obj): - if isinstance(obj.content_object, PartnerActivity) \ - and obj.content_object.project: - return obj.content_object.project.title - else: - return "" + if isinstance(obj.content_object, PartnerActivity): + if obj.content_object.projects.exists(): + return obj.content_object.projects.first().title + + return "" def get_cluster_activity(self, obj): if isinstance(obj.content_object, PartnerActivity) \ diff --git a/django_api/django_api/apps/indicator/views.py b/django_api/django_api/apps/indicator/views.py index c140ef4db..7ba2bf153 100644 --- a/django_api/django_api/apps/indicator/views.py +++ b/django_api/django_api/apps/indicator/views.py @@ -774,7 +774,7 @@ def post(self, request, *args, **kwargs): ) try: - project_name = reportable.content_object.project.title + project_name = reportable.content_object.projects.first().title except Exception: project_name = '' diff --git a/django_api/django_api/apps/partner/models.py b/django_api/django_api/apps/partner/models.py index e53355b9f..c68aad725 100644 --- a/django_api/django_api/apps/partner/models.py +++ b/django_api/django_api/apps/partner/models.py @@ -343,11 +343,12 @@ class Meta: @property def clusters(self): - return self.project.clusters.all() + from cluster.models import Cluster + return Cluster.objects.filter(id__in=self.projects.values_list('project__clusters', flat=True).distinct()) @property def response_plan(self): - return self.project.clusters.all()[0].response_plan + return self.clusters[0].response_plan @property def is_custom(self): From 27ecbb67859d6e5c8ec3d8985de8a5faa150ebac Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Wed, 22 May 2019 17:47:55 -0700 Subject: [PATCH 424/817] fix: Have Add Project modal clear out on close and add client-side conditional for admin_level check [ch10757] --- .../projects/creation-modal.html | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html index afd0c6d68..ce1122b9b 100644 --- a/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html +++ b/polymer/src/elements/cluster-reporting/planned-action/projects/creation-modal.html @@ -231,12 +231,13 @@

[[_computeTitle(edit)]]

@@ -672,7 +673,7 @@

Fields required

Save - + Cancel
@@ -937,11 +938,20 @@

Fields required

return App.Endpoints.ochaProjectDetails(selectedProject); }, - close: function () { - this.set('data', {}); - this.set('opened', false); - this.set('refresh', false); - this.set('detailsOpened', false); + _close: function (e) { + if (e.target.nodeName === 'PAPER-DIALOG' || + e.target.nodeName === 'PAPER-BUTTON' || + e.target.nodeName === 'PAPER-ICON-BUTTON') { + this.set('data', {}); + this.set('opened', false); + this.set('refresh', false); + this.set('detailsOpened', false); + this.set('errors', {}); + + this.close(); + } else { + return; + } }, open: function () { @@ -1121,8 +1131,8 @@

Fields required

_save: function () { var self = this; + var locationError = false; var rawLocations = this.get('data.locations'); - console.log('raw locations', rawLocations); var changedLocations = rawLocations.map(function (location) { if (location.location !== undefined) { @@ -1132,16 +1142,17 @@

Fields required

} }); - var noLocationSet = false; - changedLocations.forEach(function (location) { if (location.title === undefined) { self.set('errors', 'No location set - please set a location.'); - noLocationSet = true; + locationError = true; + } else if (changedLocations[0].admin_level !== location.admin_level) { + self.set('errors', 'All locations need to have the same admin level.'); + locationError = true; } }); - if (noLocationSet === true) { + if (locationError === true) { return; } @@ -1170,8 +1181,8 @@

Fields required

}) .catch(function (err) { // jshint ignore:line self.updatePending = false; - self.set('data.locations', rawLocations); self.set('errors', err.data); + self.set('data.locations', rawLocations); }); }, From 97fec72eb419446e769366f66b0a4bf48a8dea4c Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 23 May 2019 09:14:22 -0700 Subject: [PATCH 425/817] feat: Add the rest of the labels for IP Reporting [ch11929] --- polymer/src/locales.json | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/polymer/src/locales.json b/polymer/src/locales.json index a0971180b..7087026c4 100644 --- a/polymer/src/locales.json +++ b/polymer/src/locales.json @@ -82,7 +82,7 @@ "current_progress": "Current progress", "against_target": "Against Target", "details": "Details", - "calculation_methods": "Calculation Methods", + "calculation_methods": "Calculation methods", "agreement": "Agreement", "document_type": "Document Type", "reference_number": "Reference Number", @@ -106,7 +106,35 @@ "show_incomplete_only": "Show incomplete only", "refresh": "Refresh", "generate_uploader": "Generate Uploader Template", - "import_template": "Import Template" + "import_template": "Import Template", + "pd_output_results": "PD Output / SSFA Expected Results", + "overall_status": "Overall status", + "met": "Met", + "on_track": "On Track", + "no_progress": "No Progress", + "constrained": "Constrained", + "narrative_assessment": "Narrative Assessment", + "edit": "Edit", + "save": "Save", + "sum": "sum", + "across_locations": "across locations", + "across_reporting_periods": "across reporting periods", + "total_cumulative_progress": "Total cumulative progress", + "achievement_in_reporting_period": "Achievement in reporting period", + "for_this_indicator": "For this indicator, data can be pulled from reports matching this reporting period", + "pull_data": "Pull Data from HR", + "data_for_locations": "Data for locations", + "enter_data_location": "Enter data for this location", + "location_progress_against": "Location progress against UNICEF target", + "previous_location_progress": "Previous location progress", + "total": "Total", + "partner_contribution": "Partner contribution to date", + "challenges_bottlenecks": "Challenges/bottlenecks in the reporting period (latest)", + "proposed_way_forward": "Proposed way forward (latest)", + "face": "FACE", + "other": "Other", + "upload_file": "Upload file", + "no_report_data": "No report data associated with this indicator has been submitted yet" }, "fr": { "ip_reporting": "Reporting Partenaires UNICEF", From 4c721bfcb39e6b935f134ef8ffde21c37f9b5fc7 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 23 May 2019 10:41:31 -0700 Subject: [PATCH 426/817] feat: Add labels for Cluster Reporting for both partner and cluster users [ch11929] --- polymer/src/locales.json | 137 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 3 deletions(-) diff --git a/polymer/src/locales.json b/polymer/src/locales.json index 7087026c4..f52f36158 100644 --- a/polymer/src/locales.json +++ b/polymer/src/locales.json @@ -99,7 +99,7 @@ "signed_date": "Signed Date", "reporting_requirements": "Reporting Requirements", "status": "Status", - "enter_data": "Enter data for", + "enter_data_for": "Enter data for", "reporting_on_results": "Reporting on Results", "other_info": "Other Info", "pd_output": "PD output", @@ -125,7 +125,7 @@ "pull_data": "Pull Data from HR", "data_for_locations": "Data for locations", "enter_data_location": "Enter data for this location", - "location_progress_against": "Location progress against UNICEF target", + "location_progress_against_unicef": "Location progress against UNICEF target", "previous_location_progress": "Previous location progress", "total": "Total", "partner_contribution": "Partner contribution to date", @@ -134,7 +134,138 @@ "face": "FACE", "other": "Other", "upload_file": "Upload file", - "no_report_data": "No report data associated with this indicator has been submitted yet" + "no_report_data": "No report data associated with this indicator has been submitted yet", + "partner_dashboard": "Partner Dashboard", + "partner": "Partner", + "cluster": "Cluster", + "partner_activity_indicators": "Partner Activity Indicators by Progress Status", + "number_of_projects": "Number of Projects", + "see_all_projects": "See All Projects", + "number_of_due": "Number of Due/Overdue Partner Activity Reports", + "see_all_reports": "See All Reports", + "number_of_non_cluster": "Number of non-Cluster Activities", + "list_of_overdue_indicator": "List of overdue indicator reports", + "project": "Project", + "activity": "Activity", + "enter_data": "Enter Data", + "location_progress_against_cluster": "Location progress against Cluster target", + "submitted_list_constrained": "Submitted List of Constrained Partner Activity Indicator", + "last_reported": "Last reported", + "ongoing": "Ongoing", + "cluster_activity": "Cluster Activity", + "see_more": "See More", + "objectives": "Objectives", + "cluster_activities": "Cluster Activities", + "disaggregations": "Disaggregations", + "search_cluster_objective": "Search Cluster Objective Title", + "cluster_objective_title": "Cluster Objective Title", + "search_cluster_activity": "Search Cluster Activity", + "cluster_activity_title": "Cluster Activity Title", + "disaggregation": "Disaggregation", + "disaggregation_groups": "Disaggregation Groups", + "projects": "Projects", + "partner_activities": "Partner Activities", + "from_ocha": "From OCHA", + "search_activity_title": "Search Activity Title", + "show_only_custom_activities": "Show only custom activities", + "cluster_objective": "Cluster Objective", + "custom": "Custom", + "add_project": "Add Project", + "activities": "Activities", + "add_activity": "Add Activity", + "indicator_type": "Indicator Type", + "draft_due_reports": "Draft/Due Reports", + "submitted_reports": "Submitted Reports", + "export": "Export", + "narrow_location_type": "Narrow Location Type", + "operational_presence": "Operational presence", + "number_of_clusters": "Number of Clusters", + "number_of_partners": "Number of Partners", + "partners_per_type": "Partners per Type", + "partners_per_cluster": "Partners per Cluster", + "partners_per_cluster_objective": "Partners per Cluster Objective", + "operational_presence_map": "Operational Presence Map", + "operation_presence_table": "Operational Presence Table", + "partner_project": "Partner Project", + "against_in_need": "Against In need", + "reporting_frequency": "Reporting Frequency", + "number_of_partners_reporting": "# of Partners Reporting", + "reached": "Reached", + "partners_by_status": "Partners by Status", + "progress_over_time": "Progress over time", + "current_progress_partner": "Current progress by Partner", + "current_progress_location": "Current progress by Location", + "no_data_progress_time": "No data for Progress over time yet", + "no_data_current_location": "No data for Current progress by Location yet", + "loading_data": "Loading data", + "project_indicators": "Project Indicators", + "project_activities": "Project Activities", + "edit_project": "Edit Project", + "project_details": "Project Details", + "funding_source": "Funding Source", + "description": "Description", + "additional_information": "Additional Information (e.g. links)", + "show_more_details": "Show More Details", + "name_of_agency": "Name of Agency (acronyms)", + "type_of_agency": "Type of Agency", + "additional_implementing_partners": "Additional Implementing Partners", + "prioritization_classification": "Prioritization Classification", + "project_code_hrp": "Project code in HRP", + "funding_requirements": "Funding Requirements (US$)", + "funding_received_confirmed_own_agency": "Funding received / confirmed through own agency internal emergency funding mechanisms (US$)", + "funding_received_confirmed_cerf": "Funding received / confirmed from CERF (US$)", + "funding_received_confirmed_cbpf": "Funding received / confirmed from CBPF (Country based pooled funds), e.g. CHF, ERF (US$)", + "funding_received_confirmed_bilateral": "Funding received / confirmed from bilateral agreements, not including UNICEF/WFP (US$)", + "funding_received_confirmed_unicef": "Funding received / confirmed from UNICEF including supplies cost (US$)", + "funding_received_confirmed_wfp": "Funding received / confirmed from WFP including supplies cost (US$)", + "funding_gap": "Funding gap (US$)", + "add_project_indicator": "Add Project Indicator", + "no_results_found": "No results found", + "adopt_from_cluster_objective": "Adopt from Cluster Objective", + "type": "Type", + "quantity": "Quantity (#)", + "percent": "Percent", + "ratio": "Ratio", + "calculation_method_across_locations": "Calculation method across locations", + "max": "Max", + "avg": "Avg", + "calculation_method_across_reporting": "Calculation method across reporting periods", + "comments": "Comments (optional)", + "measurement_specifications": "Measurement specifications (optional)", + "frequency_of_reporting": "Frequency of reporting", + "start_date_reporting": "Start date of reporting period", + "label": "Label", + "in_need": "In need (optional)", + "add_location": "Add Location", + "add_disaggregation": "Add Disaggregation", + "cancel": "Cancel", + "select_workspace": "Select Workspace", + "select_response_plan_sdn": "Select Response Plan SDN", + "select_response_plan": "Select Response Plan from the list below or add a new one if the needed one is not available.", + "add_new_response_plan": "Add New Response Plan", + "response_plan": "Response Plan", + "plan_type": "Plan Type", + "confirm": "Confirm", + "cluster_dashboard": "Cluster Dashboard", + "number_of_contributing_partners": "Number of Contributing Partners", + "add_cluster_objective": "Add Cluster Objective", + "add_cluster_activity": "Add Cluster Activity", + "objective": "Objective", + "cluster_objective_indicators": "Cluster Objective Indicators", + "cluster_objective_details": "Cluster Objective Details", + "edit_objective": "Edit Objective", + "edit_cluster_objective": "Edit Cluster Objective", + "add_cluster_objective_indicator": "Add Cluster Objective Indicator", + "basic_project_details": "Basic Project details", + "fields_required": "Fields required", + "want_to_add": "Want to add more details to the Project info?", + "show_more_funding_details": "Show More Funding Details", + "name_of_funding_agency": "Name of Funding Agency (acronyms)", + "is_this_project_hrp_fa": "Is this project part of a HRP or FA?", + "no": "No", + "add_custom_field": "Add Custom Field", + "add_from_cluster_activities": "Add from Cluster Activities", + "add_custom_activity": "Add Custom Activity" }, "fr": { "ip_reporting": "Reporting Partenaires UNICEF", From fa4e92f57f90a1f662d45789365aba4a3763ca1c Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 23 May 2019 11:11:48 -0700 Subject: [PATCH 427/817] feat: Add labels for ID Management for both partner and cluster users [ch11929] --- polymer/src/locales.json | 72 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/polymer/src/locales.json b/polymer/src/locales.json index f52f36158..563fb69dc 100644 --- a/polymer/src/locales.json +++ b/polymer/src/locales.json @@ -265,7 +265,77 @@ "no": "No", "add_custom_field": "Add Custom Field", "add_from_cluster_activities": "Add from Cluster Activities", - "add_custom_activity": "Add Custom Activity" + "add_custom_activity": "Add Custom Activity", + "user_management": "User Management", + "users": "Users", + "search": "Search", + "name_or_email": "Name or Email", + "role": "Role", + "select_role": "Select role", + "invited": "Invited", + "inactive": "Inactive", + "select_cluster": "Select cluster", + "email": "E-mail", + "last_login": "Last login", + "user_type": "User type", + "partner_user": "Partner user", + "add_new_permission": "Add New Permission", + "add_permissions": "Add permissions", + "role_per_cluster": "Role per Cluster", + "add_new": "Add New", + "add_new_user": "Add new user", + "message_with_invitation": "Message with invitation will be sent to the provided e-mail address", + "first_name": "First name", + "provide_first_name": "Provide first name", + "last_name": "Last name", + "provide_last_name": "Provide last name", + "provide_email": "Provide e-mail", + "position": "Position (optional)", + "provide_position": "Provide position (optional)", + "select_user_type": "Select user type", + "save_and_continue": "Save and Continue", + "select_partner_type": "Select partner type", + "external_id": "External ID", + "shared_partner": "Shared partner", + "cso_type": "CSO Type", + "street_address": "Street address", + "city": "City", + "country_code": "Country code", + "risk_rating": "Risk rating", + "basis_risk_rating": "Basis for risk rating", + "add_new_partner": "Add new partner", + "ocha_id": "OCHA ID", + "provide_ocha_id": "Provide OCHA ID", + "provide_external_id": "Provide external ID", + "an_id_representing": "An ID representing this instance in an external system", + "external_source": "External source", + "provide_external_source": "Provide external source", + "provide_full_name": "Provide full name", + "please_make_sure": "Please make sure this matches the name you enter in VISION", + "short_title": "Short title", + "provide_short_title": "Provide short title", + "alternate_title": "Alternate title", + "provide_alternate_title": "Provide alternate title", + "select_shared_partner": "Select shared partner", + "select_cso_type": "Select CSO type", + "provide_phone_number": "Provide phone number", + "provide_street_address": "Provide street address", + "provide_city": "Provide city", + "postal_code": "Postal code", + "provide_postal_code": "Provide postal code", + "provide_country_code": "Provide country code", + "total_ct_cp": "Total CT CP", + "provide_total_ct_cp": "Provide total CT CP", + "total_cash_ct": "Total Cash Transferred for Country Programme", + "total_ct_cy": "Total CT CY", + "provide_total_ct_cy": "Provide total CT CY", + "total_cash_cy": "Total Cash Transferred per Current Year", + "provide_vendor_number": "Provide vendor number", + "alternate_id": "Alternate ID", + "provide_alternate_id": "Provide alternate ID", + "provide_risk_rating": "Provide risk rating", + "provide_basis_risk_rating": "Provide basis for risk rating", + "edit_partner": "Edit Partner" }, "fr": { "ip_reporting": "Reporting Partenaires UNICEF", From 4fcedc19990a0654869a505926b7d82242b57ba2 Mon Sep 17 00:00:00 2001 From: Silin Na Date: Thu, 23 May 2019 11:50:55 -0700 Subject: [PATCH 428/817] lint: Fixed JSLint error messages --- .../cluster-reporting/indicator-editing-modal.html | 1 - .../indicator-locations-widget.html | 12 +++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html index b5fcee40d..9df302fb3 100644 --- a/polymer/src/elements/cluster-reporting/indicator-editing-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-editing-modal.html @@ -553,7 +553,6 @@

Edit Indicator

var changedLocations = rawLocations.map(function (location) { if (location.location !== undefined && location.location.id !== undefined) { var id = location.location.id; - var title = location.location.title; location.location = id; return location; } else if (location.loc_type !== undefined && location.location === undefined) { diff --git a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html index 28c7b5c2e..1b2180b89 100644 --- a/polymer/src/elements/cluster-reporting/indicator-locations-widget.html +++ b/polymer/src/elements/cluster-reporting/indicator-locations-widget.html @@ -507,8 +507,6 @@

Locations ([[value.length]])

this.set('locations', newLocations); - var pendingTest = this.get('pending'); - var newPendingIndex = Object.keys(this.get('pending')).length; var pending = this.get('pending'); @@ -563,12 +561,12 @@

Locations ([[value.length]])

} this.set('searchLocationType', loc_type); - + this.debounce('fetch-locations-' + loc_type, function () { var self = this; - + this._setPending(loc_type, true, index); - + if (title !== undefined) { this.$$('#locations' + loc_type).params.title = title; } @@ -625,11 +623,11 @@

Locations ([[value.length]])

var loc_type = location.loc_type ? location.loc_type : location.admin_level; var allLocations = this._getLocations(locations, loc_type, index); - var location = allLocations.find(function (loc) { + var targetLocation = allLocations.find(function (loc) { return String(loc.id) === String(locationId); }); - return location ? location.title : ''; + return targetLocation ? targetLocation.title : ''; }, _setPending: function (loc_type, value, index) { From 9bccf31a564d1abfa9e98d4cf2ee383c7ce6659f Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 23 May 2019 14:48:50 -0700 Subject: [PATCH 429/817] fix: Add dom-if template to render form without radio options if partnerID is undefined [ch12038] --- .../cluster-reporting/indicator-modal.html | 833 +++++++++++------- 1 file changed, 538 insertions(+), 295 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 654fc459c..5062028ba 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -167,342 +167,580 @@

[[modalTitle]]

restamp="true"> - - - - Adopt from Cluster Objective - - -
- +
+
+ + + + + + +
+
+ +
+ + + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
- + + @@ -748,6 +986,11 @@

[[modalTitle]]

return; } + if (this.partnerID === undefined) { + this.set('partnerID', false); + } + console.log(this.partnerID); + if (this.mode === undefined) { return; } else if (this.mode === 'objectives') { From 3d7ee474bb9dcf9e9967704fa87258d2f2d6b253 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 23 May 2019 14:54:05 -0700 Subject: [PATCH 430/817] fix: Remove console messages, tweak styling, and set mode to admin [ch12038] --- .../src/elements/cluster-reporting/indicator-modal.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/polymer/src/elements/cluster-reporting/indicator-modal.html b/polymer/src/elements/cluster-reporting/indicator-modal.html index 5062028ba..ba2a8800f 100644 --- a/polymer/src/elements/cluster-reporting/indicator-modal.html +++ b/polymer/src/elements/cluster-reporting/indicator-modal.html @@ -96,6 +96,10 @@ padding-top: 24px; } + #custom-form-only { + padding-top: 20px; + } + .calculation-method:not(:first-child) { margin-left: 50px; } @@ -515,7 +519,7 @@

[[modalTitle]]

if="[[!partnerID]]" restamp="true"> -
+
[[modalTitle]] if (this.partnerID === undefined) { this.set('partnerID', false); + this.set('mode', 'admin'); } - console.log(this.partnerID); if (this.mode === undefined) { return; From d7fa9965d9097a6739fd022ef3283cb97ba46186 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Thu, 23 May 2019 15:02:55 -0700 Subject: [PATCH 431/817] feat: Add snackbar messages and account creation message to localization file [ch11929] --- polymer/src/locales.json | 10 +++++++++- polymer/src/pages/app.html | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/polymer/src/locales.json b/polymer/src/locales.json index 563fb69dc..5abee6732 100644 --- a/polymer/src/locales.json +++ b/polymer/src/locales.json @@ -335,7 +335,15 @@ "provide_alternate_id": "Provide alternate ID", "provide_risk_rating": "Provide risk rating", "provide_basis_risk_rating": "Provide basis for risk rating", - "edit_partner": "Edit Partner" + "edit_partner": "Edit Partner", + "changes_saved": "Changes saved", + "an_error_occurred": "An error occurred, please try again later.", + "file_uploaded": "File uploaded.", + "file_deleted": "File deleted.", + "request_ocha_timed_out": "Request to OCHA timed out. Please try again.", + "message_sent": "Message sent", + "account_created": "Account created.", + "please_wait_business_days": "Please wait 1-2 business days for your admin to approve you. If it has been longer then 1-2 business days, please email X to get you approved." }, "fr": { "ip_reporting": "Reporting Partenaires UNICEF", diff --git a/polymer/src/pages/app.html b/polymer/src/pages/app.html index 8a625286d..46dd1a093 100644 --- a/polymer/src/pages/app.html +++ b/polymer/src/pages/app.html @@ -117,7 +117,7 @@

Account created.

From 8d12c8711666a2b92356a75464e7e8ef04c8a467 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 24 May 2019 14:00:51 -0700 Subject: [PATCH 432/817] fix: Attempt to add manual update method for updating char-counter --- polymer/src/elements/reportable-meta.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/polymer/src/elements/reportable-meta.html b/polymer/src/elements/reportable-meta.html index 08042f8b3..064be6520 100644 --- a/polymer/src/elements/reportable-meta.html +++ b/polymer/src/elements/reportable-meta.html @@ -3,6 +3,7 @@ + @@ -199,6 +200,14 @@ }, }, + _updateCharCounter: function (event) { + console.log('event', event); + var input = this.$$('#narrative_assessment'); + var inputContainer = input.parentElement; + var charCounter = input.nextElementSibling; + console.log('input', charCounter.update(inputContainer)); + }, + _handleInput: function () { var field = Polymer.dom(event).rootTarget; var narrativeTextInput = Polymer.dom(this.root).querySelectorAll('labelled-item')[1].querySelector('input'); From 53e9ef8c50f0eb00bcc43d088aa8ef971a58e083 Mon Sep 17 00:00:00 2001 From: Injoong Yoon <27440940+AttilaTheHen@users.noreply.github.com> Date: Fri, 24 May 2019 14:23:11 -0700 Subject: [PATCH 433/817] fix: Use paper-input component rather than input to use built-in char counter and add styling --- polymer/src/elements/reportable-meta.html | 40 ++++++++++------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/polymer/src/elements/reportable-meta.html b/polymer/src/elements/reportable-meta.html index 064be6520..7fe0d4452 100644 --- a/polymer/src/elements/reportable-meta.html +++ b/polymer/src/elements/reportable-meta.html @@ -1,8 +1,6 @@ - - @@ -17,6 +15,11 @@ + + +
  • - [[activityData.title]] + [[activity.title]]
  • -