From a1c4d9c43fa593bd0518b4329da67647a40b14e6 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Thu, 25 Jan 2018 17:12:17 +0100 Subject: [PATCH 01/19] make FRONTEND_HOST configurable through .env while defaulting to DJANGO_ALLOWED_HOST --- .env.example | 1 + backend/unpp_api/apps/common/utils.py | 12 ++++++++++++ backend/unpp_api/apps/project/models.py | 4 ++-- backend/unpp_api/settings/base.py | 2 +- backend/unpp_api/settings/dev.py | 1 - backend/unpp_api/settings/prod.py | 1 - backend/unpp_api/settings/staging.py | 1 - 7 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index d029d09bb..17a1a3e5c 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,4 @@ POSTGRES_SSL_MODE=off EMAIL_HOST_USER=TBD EMAIL_HOST_PASSWORD=TBD GOOGLE_KEY=GOOGLE_KEY_HERE +UNPP_FRONTEND_HOST=127.0.0.1:8080 diff --git a/backend/unpp_api/apps/common/utils.py b/backend/unpp_api/apps/common/utils.py index b01e5697c..6104ebda1 100644 --- a/backend/unpp_api/apps/common/utils.py +++ b/backend/unpp_api/apps/common/utils.py @@ -2,6 +2,7 @@ from logging.handlers import RotatingFileHandler +from django.conf import settings from imagekit import ImageSpec from imagekit.processors import ResizeToFill @@ -80,3 +81,14 @@ def confirm(prompt='Confirm', default=False): return True if ans.lower() == 'n': return False + + +def get_absolute_frontend_url(relative_url): + if not relative_url.startswith('/'): + relative_url = '/' + relative_url + + host = settings.FRONTEND_HOST + if host.endswith('/'): + host = host[:-1] + + return 'http://' + host + relative_url diff --git a/backend/unpp_api/apps/project/models.py b/backend/unpp_api/apps/project/models.py index 099fdc2b8..656552775 100644 --- a/backend/unpp_api/apps/project/models.py +++ b/backend/unpp_api/apps/project/models.py @@ -15,7 +15,7 @@ COMPLETED_REASON, EXTENDED_APPLICATION_STATUSES, ) -from common.utils import get_countries_code_from_queryset +from common.utils import get_countries_code_from_queryset, get_absolute_frontend_url from validators import ( validate_weight_adjustments, ) @@ -114,7 +114,7 @@ def is_weight_adjustments_ok(self): return sum(map(lambda x: x.get('weight'), self.assessments_criteria)) == 100 if self.has_weighting else True def get_absolute_url(self): - return "{}cfei/open/{}/overview".format(settings.FRONTEND_URL, self.id) + return get_absolute_frontend_url("/cfei/open/{}/overview".format(self.pk)) class Pin(TimeStampedModel): diff --git a/backend/unpp_api/settings/base.py b/backend/unpp_api/settings/base.py index 15f0844e9..a1c39a18f 100644 --- a/backend/unpp_api/settings/base.py +++ b/backend/unpp_api/settings/base.py @@ -71,7 +71,7 @@ DOMAIN_NAME = os.getenv('DJANGO_ALLOWED_HOST', 'localhost') WWW_ROOT = 'http://%s/' % DOMAIN_NAME ALLOWED_HOSTS = [DOMAIN_NAME] -FRONTEND_URL = 'http://127.0.0.1:8080/' +FRONTEND_HOST = os.getenv('UNPP_FRONTEND_HOST', DOMAIN_NAME) DATABASES = { 'default': { diff --git a/backend/unpp_api/settings/dev.py b/backend/unpp_api/settings/dev.py index fcc04158d..2e81f7cd1 100644 --- a/backend/unpp_api/settings/dev.py +++ b/backend/unpp_api/settings/dev.py @@ -11,7 +11,6 @@ DOMAIN_NAME = 'localhost:8000' WWW_ROOT = 'http://%s/' % DOMAIN_NAME ALLOWED_HOSTS = ["localhost", "127.0.0.1"] -FRONTEND_URL = 'http://127.0.0.1:8080/' # other EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/backend/unpp_api/settings/prod.py b/backend/unpp_api/settings/prod.py index 800e40864..23c6d45f6 100644 --- a/backend/unpp_api/settings/prod.py +++ b/backend/unpp_api/settings/prod.py @@ -5,7 +5,6 @@ # dev overrides DEBUG = False IS_STAGING = False -FRONTEND_URL = 'http://unpp.tivixlabs.com/' extend_list_avoid_repeats(INSTALLED_APPS, [ 'rest_framework_swagger', diff --git a/backend/unpp_api/settings/staging.py b/backend/unpp_api/settings/staging.py index 26b522789..ffe6649c0 100644 --- a/backend/unpp_api/settings/staging.py +++ b/backend/unpp_api/settings/staging.py @@ -5,7 +5,6 @@ # dev overrides DEBUG = True IS_STAGING = True -FRONTEND_URL = 'http://unpp-stage.tivixlabs.com/' extend_list_avoid_repeats(INSTALLED_APPS, [ 'rest_framework_swagger', From 39960f2eddf9a9d644a582cc3048d70105faf3fb Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Thu, 25 Jan 2018 17:15:57 +0100 Subject: [PATCH 02/19] Added comments to .env example --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 17a1a3e5c..efd0633c7 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,5 @@ POSTGRES_SSL_MODE=off EMAIL_HOST_USER=TBD EMAIL_HOST_PASSWORD=TBD GOOGLE_KEY=GOOGLE_KEY_HERE -UNPP_FRONTEND_HOST=127.0.0.1:8080 +DJANGO_ALLOWED_HOST=127.0.0.1 # Optional, defaults to localhost +UNPP_FRONTEND_HOST=127.0.0.1:8080 # Optional, defaults to DJANGO_ALLOWED_HOST From ad92481f397e5f49378f5fb09bb7aab46e966e3f Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Thu, 25 Jan 2018 17:16:36 +0100 Subject: [PATCH 03/19] remove unused import --- backend/unpp_api/apps/project/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/unpp_api/apps/project/models.py b/backend/unpp_api/apps/project/models.py index 656552775..2680f6d0d 100644 --- a/backend/unpp_api/apps/project/models.py +++ b/backend/unpp_api/apps/project/models.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from datetime import date -from django.conf import settings from django.contrib.postgres.fields import ArrayField, JSONField from django.db import models from model_utils.models import TimeStampedModel From 96e451f20c69de68b4b02a6af7a456b228b4b7a5 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Fri, 26 Jan 2018 11:22:54 +0100 Subject: [PATCH 04/19] Use bcc when notifying applicants of CFEI changes --- backend/unpp_api/apps/notification/helpers.py | 41 +++++++++---------- .../CN_Assessment_not_successful | 2 +- backend/unpp_api/apps/project/views.py | 4 +- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/backend/unpp_api/apps/notification/helpers.py b/backend/unpp_api/apps/notification/helpers.py index f982d99b7..925ab0280 100644 --- a/backend/unpp_api/apps/notification/helpers.py +++ b/backend/unpp_api/apps/notification/helpers.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta from django.db import transaction -from django.core.mail import send_mail +from django.core.mail import send_mail, EmailMessage from django.conf import settings from django.template import loader from django.contrib.contenttypes.models import ContentType @@ -33,7 +33,7 @@ def feed_alert(notification_type, subject, body, users, obj): def send_notification( - notification_type, obj, users, context=None, send_in_feed=True, check_sent_for_source=True + notification_type, obj, users, context=None, send_in_feed=True, check_sent_for_source=True, use_bcc=False ): """ notification_type - check NotificationType class in const.py @@ -52,7 +52,16 @@ def send_notification( targets = [u.email for u in users] body = render_notification_template_to_str(notification_info.get('template_name'), context) - send_mail(notification_info.get('subject'), body, settings.DEFAULT_FROM_EMAIL, targets) + + to, bcc = ([], targets) if use_bcc else (targets, []) + + EmailMessage( + subject=notification_info.get('subject'), + body=body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=to, + bcc=bcc + ).send() if send_in_feed: return feed_alert(notification_type, notification_info.get('subject'), body, users, obj) @@ -63,17 +72,11 @@ def render_notification_template_to_str(template_name, context): def get_notify_partner_users_for_application(application): - users = User.objects.filter(partner_members__partner=application.partner) - return users.distinct() + return User.objects.filter(partner_members__partner=application.partner).distinct() -def get_partner_users_for_app_qs(application_qs): - notify_user_ids = [] - for application in application_qs: - users_qs = get_notify_partner_users_for_application(application) - notify_user_ids.extend(list(users_qs.values_list('id', flat=True))) - - return User.objects.filter(id__in=notify_user_ids) +def get_partner_users_for_application_queryset(application_qs): + return User.objects.filter(partner_members__partner__applications__in=application_qs).distinct() # We don't want to send 2x of the same notification @@ -97,16 +100,12 @@ def user_received_notification_recently(user, obj, notification_type, time_ago=r def send_notification_cfei_completed(eoi): if eoi.completed_reason == COMPLETED_REASON.canceled: - users = get_partner_users_for_app_qs(eoi.applications.all()) - send_notification(NotificationType.CFEI_CANCELLED, eoi, users) - - if eoi.completed_reason == COMPLETED_REASON.partners: - users = get_partner_users_for_app_qs(eoi.applications.losers()) - send_notification(NotificationType.CFEI_APPLICATION_LOSS, eoi, users) + users = get_partner_users_for_application_queryset(eoi.applications.all()) + send_notification(NotificationType.CFEI_CANCELLED, eoi, users, use_bcc=True) - if eoi.completed_reason == COMPLETED_REASON.no_candidate: - users = get_partner_users_for_app_qs(eoi.applications.all()) - send_notification(NotificationType.CFEI_APPLICATION_LOSS, eoi, users) + if eoi.completed_reason in {COMPLETED_REASON.partners, COMPLETED_REASON.no_candidate}: + users = get_partner_users_for_application_queryset(eoi.applications.losers()) + send_notification(NotificationType.CFEI_APPLICATION_LOSS, eoi, users, use_bcc=True) def send_agency_updated_application_notification(application): diff --git a/backend/unpp_api/apps/notification/templates/notifications/CN_Assessment_not_successful b/backend/unpp_api/apps/notification/templates/notifications/CN_Assessment_not_successful index 869efe358..9547fdaa4 100644 --- a/backend/unpp_api/apps/notification/templates/notifications/CN_Assessment_not_successful +++ b/backend/unpp_api/apps/notification/templates/notifications/CN_Assessment_not_successful @@ -6,4 +6,4 @@ The review of applications has now concluded. Unfortunately, your organization Please continue to visit the UN Partner Portal for other partnership opportunities. -Thank you. \ No newline at end of file +Thank you. diff --git a/backend/unpp_api/apps/project/views.py b/backend/unpp_api/apps/project/views.py index f6b2a26f5..6e257f527 100644 --- a/backend/unpp_api/apps/project/views.py +++ b/backend/unpp_api/apps/project/views.py @@ -39,7 +39,7 @@ from common.mixins import PartnerIdsMixin from notification.consts import NotificationType from notification.helpers import ( - get_partner_users_for_app_qs, + get_partner_users_for_application_queryset, send_notification_cfei_completed, send_agency_updated_application_notification, send_notification_application_created, @@ -152,7 +152,7 @@ def perform_update(self, serializer): # Deadline Changed if current_deadline != instance.deadline_date: - users = get_partner_users_for_app_qs(instance.applications.all()) + users = get_partner_users_for_application_queryset(instance.applications.all()) context = { 'initial_date': current_deadline, 'revised_date': instance.deadline_date, From 88399b1f53724b3ea030859fd7d8228098a7256e Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Fri, 26 Jan 2018 11:26:54 +0100 Subject: [PATCH 05/19] remove unused import --- backend/unpp_api/apps/notification/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/unpp_api/apps/notification/helpers.py b/backend/unpp_api/apps/notification/helpers.py index 925ab0280..fb285be32 100644 --- a/backend/unpp_api/apps/notification/helpers.py +++ b/backend/unpp_api/apps/notification/helpers.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta from django.db import transaction -from django.core.mail import send_mail, EmailMessage +from django.core.mail import EmailMessage from django.conf import settings from django.template import loader from django.contrib.contenttypes.models import ContentType From 452f9dbb77415960a73f0091093880f04e2e6fc7 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Fri, 26 Jan 2018 11:40:06 +0100 Subject: [PATCH 06/19] fix overeager refactor --- backend/unpp_api/apps/notification/helpers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/unpp_api/apps/notification/helpers.py b/backend/unpp_api/apps/notification/helpers.py index fb285be32..73fb08aa0 100644 --- a/backend/unpp_api/apps/notification/helpers.py +++ b/backend/unpp_api/apps/notification/helpers.py @@ -103,10 +103,14 @@ def send_notification_cfei_completed(eoi): users = get_partner_users_for_application_queryset(eoi.applications.all()) send_notification(NotificationType.CFEI_CANCELLED, eoi, users, use_bcc=True) - if eoi.completed_reason in {COMPLETED_REASON.partners, COMPLETED_REASON.no_candidate}: + if eoi.completed_reason == COMPLETED_REASON.partners: users = get_partner_users_for_application_queryset(eoi.applications.losers()) send_notification(NotificationType.CFEI_APPLICATION_LOSS, eoi, users, use_bcc=True) + if eoi.completed_reason == COMPLETED_REASON.no_candidate: + users = get_partner_users_for_application_queryset(eoi.applications.all()) + send_notification(NotificationType.CFEI_APPLICATION_LOSS, eoi, users, use_bcc=True) + def send_agency_updated_application_notification(application): if application.eoi.status == EOI_STATUSES.open: From 023818532f619267e0a63c02f8ab3960f1a47187 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Fri, 26 Jan 2018 12:24:43 +0100 Subject: [PATCH 07/19] Added applications count to applications-to-score --- backend/unpp_api/apps/project/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/unpp_api/apps/project/serializers.py b/backend/unpp_api/apps/project/serializers.py index a72a1c075..a6f053b49 100644 --- a/backend/unpp_api/apps/project/serializers.py +++ b/backend/unpp_api/apps/project/serializers.py @@ -223,6 +223,10 @@ def validate(self, data): class ApplicationFullEOISerializer(ApplicationFullSerializer): eoi = BaseProjectSerializer(read_only=True) + eoi_applications_count = serializers.SerializerMethodField(allow_null=True, read_only=True) + + def get_eoi_applications_count(self, application): + return application.eoi.applications.count() class CreateUnsolicitedProjectSerializer(MixinPreventManyCommonFile, serializers.Serializer): From db7aa7cbe8f9e27821dc67d3858246edbf5da048 Mon Sep 17 00:00:00 2001 From: Adam Podsiadlo Date: Fri, 26 Jan 2018 13:40:04 +0100 Subject: [PATCH 08/19] Fix specializations, remove direct filter status --- .../direct/partnerApplicationsDirectFilter.js | 66 ++++--------------- .../partnerApplicationsUnsolicitedFilter.js | 24 ++++--- 2 files changed, 30 insertions(+), 60 deletions(-) diff --git a/frontend/src/components/applications/direct/partnerApplicationsDirectFilter.js b/frontend/src/components/applications/direct/partnerApplicationsDirectFilter.js index d06dd0b8e..63c714be9 100644 --- a/frontend/src/components/applications/direct/partnerApplicationsDirectFilter.js +++ b/frontend/src/components/applications/direct/partnerApplicationsDirectFilter.js @@ -8,10 +8,9 @@ import { withStyles } from 'material-ui/styles'; import Grid from 'material-ui/Grid'; import Button from 'material-ui/Button'; import SelectForm from '../../forms/selectForm'; -import RadioForm from '../../forms/radioForm'; import Agencies from '../../forms/fields/projectFields/agencies'; import CountryField from '../../forms/fields/projectFields/locationField/countryField'; -import { selectMappedSpecializations, selectNormalizedCountries, selectNormalizedApplicationStatuses } from '../../../store'; +import { selectMappedSpecializations } from '../../../store'; import resetChanges from '../../eois/filters/eoiHelper'; const messages = { @@ -20,8 +19,6 @@ const messages = { search: 'Search', country: 'Country', location: 'Location - Admin 1', - status: 'CFEI Status', - cnStatus: 'CN Status', agency: 'Agency', sector: 'Sector & Area of Specialization', fromDate: 'From date', @@ -43,17 +40,6 @@ const styleSheet = theme => ({ }, }); -export const STATUS_VAL = [ - { - value: true, - label: 'Active', - }, - { - value: false, - label: 'Completed', - }, -]; - class PartnerApplicationsNotesFilter extends Component { constructor(props) { super(props); @@ -67,15 +53,10 @@ class PartnerApplicationsNotesFilter extends Component { componentWillMount() { const { pathName, query } = this.props; - resetChanges(pathName, query); - - const active = !!(this.props.query.cfei_active === 'true' || (typeof (this.props.query.cfei_active) === 'boolean' && this.props.query.cfei_active) || !this.props.query.cfei_active); history.push({ pathname: pathName, - query: R.merge(query, - { cfei_active: active }, - ), + query, }); } @@ -83,13 +64,9 @@ class PartnerApplicationsNotesFilter extends Component { if (R.isEmpty(nextProps.query)) { const { pathname } = nextProps.location; - const active = this.props.query.cfei_active ? this.props.query.cfei_active : true; - history.push({ pathname, - query: R.merge(this.props.query, - { cfei_active: active }, - ), + query: this.props.query, }); } } @@ -98,16 +75,15 @@ class PartnerApplicationsNotesFilter extends Component { onSearch(values) { const { pathName, query } = this.props; - const { agency, country_code, specialization, cfei_active } = values; + const { agency, country_code, specializations } = values; history.push({ pathname: pathName, query: R.merge(query, { page: 1, agency, - cfei_active, country_code, - specialization, + specializations: Array.isArray(specializations) ? specializations.join(',') : specializations, }), }); } @@ -119,14 +95,12 @@ class PartnerApplicationsNotesFilter extends Component { history.push({ pathname: pathName, - query: R.merge(query, - { cfei_active: true }, - ), + query, }); } render() { - const { classes, countryCode, countries, specs, handleSubmit, cnStatus, reset } = this.props; + const { classes, countryCode, specs, handleSubmit, reset } = this.props; return (
@@ -144,8 +118,9 @@ class PartnerApplicationsNotesFilter extends Component { @@ -158,16 +133,6 @@ class PartnerApplicationsNotesFilter extends Component { /> - - - - - @@ -183,7 +193,6 @@ PartnerApplicationsUnsolicitedFilter.propTypes = { */ reset: PropTypes.func.isRequired, classes: PropTypes.object.isRequired, - countries: PropTypes.array.isRequired, specs: PropTypes.array.isRequired, pathName: PropTypes.string, query: PropTypes.object, @@ -210,7 +219,6 @@ const mapStateToProps = (state, ownProps) => { R.map(Number, specializations.split(',')); return { - countries: selectNormalizedCountries(state), specs: selectMappedSpecializations(state), directSources: selectNormalizedDirectSelectionSource(state), pathName: ownProps.location.pathname, @@ -220,7 +228,7 @@ const mapStateToProps = (state, ownProps) => { project_title, country_code, agency: agencyQ, - specialization: specializationsQ, + specializations: specializationsQ, selected_source, ds_converted, }, From 5f9de1e3c69bc64d21d70228bf365d9b4127ee28 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Mon, 29 Jan 2018 09:30:35 +0100 Subject: [PATCH 09/19] Hide reviews when reviewer is removed --- backend/unpp_api/apps/project/admin.py | 11 ++++++-- .../migrations/0044_auto_20180126_1325.py | 26 +++++++++++++++++++ backend/unpp_api/apps/project/models.py | 19 ++++++++++---- backend/unpp_api/apps/project/serializers.py | 10 ++++++- 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 backend/unpp_api/apps/project/migrations/0044_auto_20180126_1325.py diff --git a/backend/unpp_api/apps/project/admin.py b/backend/unpp_api/apps/project/admin.py index 3117c93e1..dfcd9ce92 100644 --- a/backend/unpp_api/apps/project/admin.py +++ b/backend/unpp_api/apps/project/admin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.contrib import admin -from .models import ( +from project.models import ( EOI, Pin, Application, @@ -9,8 +9,15 @@ Assessment, ) + +class ApplicationAdmin(admin.ModelAdmin): + search_fields = ('partner__legal_name', 'eoi__title') + list_display = ('id', 'partner', 'eoi', 'agency', 'did_win', 'did_accept') + list_filter = ('is_unsolicited', 'agency', 'status', 'did_win', 'did_accept', 'did_decline', 'did_withdraw') + + admin.site.register(EOI) admin.site.register(Pin) -admin.site.register(Application) +admin.site.register(Application, ApplicationAdmin) admin.site.register(ApplicationFeedback) admin.site.register(Assessment) diff --git a/backend/unpp_api/apps/project/migrations/0044_auto_20180126_1325.py b/backend/unpp_api/apps/project/migrations/0044_auto_20180126_1325.py new file mode 100644 index 000000000..c2db956f8 --- /dev/null +++ b/backend/unpp_api/apps/project/migrations/0044_auto_20180126_1325.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.8 on 2018-01-26 13:25 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('project', '0043_auto_20180123_1101'), + ] + + operations = [ + migrations.AddField( + model_name='assessment', + name='archived', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='application', + name='ds_justification_select', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('Kno', 'Known expertise'), ('Loc', 'Local presence'), ('Inn', 'Innovative approach'), ('TCC', 'Time constraints/criticality of response'), ('Imp', 'Importance of strengthening national civil society engagement'), ('Oth', 'Other')], max_length=3), blank=True, default=list, null=True, size=None), + ), + ] diff --git a/backend/unpp_api/apps/project/models.py b/backend/unpp_api/apps/project/models.py index 099fdc2b8..868feb192 100644 --- a/backend/unpp_api/apps/project/models.py +++ b/backend/unpp_api/apps/project/models.py @@ -48,10 +48,8 @@ class EOI(TimeStampedModel): deadline_date = models.DateField(verbose_name='Estimated Deadline Date', null=True, blank=True) notif_results_date = models.DateField(verbose_name='Notification of Results Date', null=True, blank=True) has_weighting = models.BooleanField(default=True, verbose_name='Has weighting?') - invited_partners = \ - models.ManyToManyField('partner.Partner', related_name="expressions_of_interest", blank=True) - reviewers = \ - models.ManyToManyField('account.User', related_name="eoi_as_reviewer", blank=True) + invited_partners = models.ManyToManyField('partner.Partner', related_name="expressions_of_interest", blank=True) + reviewers = models.ManyToManyField('account.User', related_name="eoi_as_reviewer", blank=True) justification = models.TextField(null=True, blank=True) # closed or completed completed_reason = models.CharField(max_length=3, choices=COMPLETED_REASON, null=True, blank=True) completed_date = models.DateTimeField(null=True, blank=True) @@ -163,7 +161,8 @@ class Application(TimeStampedModel): ds_justification_select = ArrayField( models.CharField(max_length=3, choices=JUSTIFICATION_FOR_DIRECT_SELECTION), default=list, - null=True + null=True, + blank=True, ) # Applies when application converted to EOI. Only applicable if this is unsolicited eoi_converted = models.OneToOneField(EOI, related_name="unsolicited_conversion", null=True, blank=True) @@ -263,6 +262,12 @@ def __str__(self): return "ApplicationFeedback ".format(self.id) +class AssessmentManager(models.Manager): + + def get_queryset(self): + return super(AssessmentManager, self).get_queryset().filter(archived=False) + + class Assessment(TimeStampedModel): created_by = models.ForeignKey('account.User', related_name="assessments_creator") modified_by = models.ForeignKey('account.User', related_name="assessments_editor", null=True, blank=True) @@ -271,6 +276,7 @@ class Assessment(TimeStampedModel): scores = JSONField(default=[dict((('selection_criteria', None), ('score', 0)))]) date_reviewed = models.DateField(auto_now=True, verbose_name='Date reviewed') note = models.TextField(null=True, blank=True) + archived = models.BooleanField(default=False) class Meta: ordering = ['id'] @@ -279,6 +285,9 @@ class Meta: def __str__(self): return "Assessment ".format(self.id) + objects = AssessmentManager() + all_objects = models.Manager() + __total_score = None @property diff --git a/backend/unpp_api/apps/project/serializers.py b/backend/unpp_api/apps/project/serializers.py index a72a1c075..a66925427 100644 --- a/backend/unpp_api/apps/project/serializers.py +++ b/backend/unpp_api/apps/project/serializers.py @@ -483,9 +483,14 @@ def update(self, instance, validated_data): reviewers = self.initial_data.get('reviewers', []) if reviewers: + Assessment.objects.filter( + application__eoi=instance + ).exclude(reviewer_id__in=reviewers).update(archived=True) instance.reviewers.through.objects.exclude(user_id__in=reviewers).delete() instance.reviewers.add(*User.objects.filter(id__in=reviewers)) + Assessment.all_objects.filter(application__eoi=instance, reviewer_id__in=reviewers).update(archived=False) elif 'reviewers' in self.initial_data: + Assessment.objects.filter(application__eoi=instance).update(archived=True) instance.reviewers.clear() focal_points = self.initial_data.get('focal_points', []) @@ -571,7 +576,10 @@ def get_your_score_breakdown(self, obj): return my_assessment.get_scores_as_dict() if my_assessment else None def get_review_progress(self, obj): - return "{}/{}".format(obj.assessments.count(), obj.eoi.reviewers.count()) + review_count = obj.assessments.count() + reviewers_count = obj.eoi.reviewers.count() + + return '{}/{}'.format(review_count, reviewers_count) class ReviewersApplicationSerializer(serializers.ModelSerializer): From 6de3526e68c7dcec78ac2c4b69380a3d98ec2b07 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Mon, 29 Jan 2018 10:11:14 +0100 Subject: [PATCH 10/19] fix ApplicationsUnsolicitedFilter --- backend/unpp_api/apps/project/filters.py | 30 ++++-------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/backend/unpp_api/apps/project/filters.py b/backend/unpp_api/apps/project/filters.py index 3003a63d5..7918a3b14 100644 --- a/backend/unpp_api/apps/project/filters.py +++ b/backend/unpp_api/apps/project/filters.py @@ -47,7 +47,6 @@ class ApplicationsFilter(django_filters.FilterSet): legal_name = CharFilter(method='get_legal_name') country_code = CharFilter(method='get_country_code') location = CharFilter(method='get_location') - specialization = CharFilter(method='get_specialization') specializations = ModelMultipleChoiceFilter(widget=CSVWidget(), name='eoi__specializations', queryset=Specialization.objects.all()) @@ -76,10 +75,6 @@ def get_country_code(self, queryset, name, value): def get_location(self, queryset, name, value): return queryset.filter(eoi__locations__admin_level_1=value) - # TODO - remove once frontend has integrated with specializations - def get_specialization(self, queryset, name, value): - return queryset.filter(eoi__specializations=value) - def get_year(self, queryset, name, value): return queryset.filter(partner__experiences__years=value) @@ -175,16 +170,15 @@ class ApplicationsUnsolicitedFilter(django_filters.FilterSet): project_title = CharFilter(method='get_project_title') country_code = CharFilter(method='get_country_code') location = CharFilter(method='get_location') - specializations = ModelMultipleChoiceFilter(widget=CSVWidget(), - method='get_specializations', - queryset=Specialization.objects.all()) - specialization = CharFilter(method='get_specialization') + specializations = ModelMultipleChoiceFilter( + widget=CSVWidget(), name='eoi__specializations', queryset=Specialization.objects.all() + ) agency = CharFilter(method='get_agency') ds_converted = BooleanFilter(method='get_ds_converted', widget=BooleanWidget()) class Meta: model = Application - fields = ['project_title', 'country_code', 'location', 'specialization', 'agency'] + fields = ['project_title', 'country_code', 'location', 'specializations', 'agency'] def get_project_title(self, queryset, name, value): return queryset.filter( @@ -192,22 +186,6 @@ def get_project_title(self, queryset, name, value): Q(eoi__title__icontains=value) # direct selection - developed from unsolicited ) - # TODO - remove once frontend has integrated with specializations - def get_specialization(self, queryset, name, value): - return queryset.filter( - Q(proposal_of_eoi_details__contains={"specialization": [value]}) | # unsolicited - Q(eoi__specializations=[value]) # direct selection - developed from unsolicited - ) - - def get_specializations(self, queryset, name, value): - if value: - value = list(value.values_list('id', flat=True)) - query = Q() - for pk in value: - query |= Q(proposal_of_eoi_details__specializations__contains=pk) - return queryset.filter(query) - return queryset - def get_country_code(self, queryset, name, value): return queryset.filter( Q(locations_proposal_of_eoi__admin_level_1__country_code=value) | # unsolicited From 55e18075bf4ab42dc0a5645b1059b73cb6dbee38 Mon Sep 17 00:00:00 2001 From: Adam Podsiadlo Date: Mon, 29 Jan 2018 10:17:51 +0100 Subject: [PATCH 11/19] Review applications tooltip --- .../eois/cells/preselectedReviewsCell.js | 61 +++++++++++++++++++ .../applications/openCfeiPreselected.js | 6 ++ .../src/reducers/partnersApplicationsList.js | 1 + 3 files changed, 68 insertions(+) create mode 100644 frontend/src/components/eois/cells/preselectedReviewsCell.js diff --git a/frontend/src/components/eois/cells/preselectedReviewsCell.js b/frontend/src/components/eois/cells/preselectedReviewsCell.js new file mode 100644 index 000000000..571fd2ffb --- /dev/null +++ b/frontend/src/components/eois/cells/preselectedReviewsCell.js @@ -0,0 +1,61 @@ +import React from 'react'; +import R from 'ramda'; +import PropTypes from 'prop-types'; +import { withStyles } from 'material-ui/styles'; +import Typography from 'material-ui/Typography'; +import Tooltip from '../../common/portalTooltip'; + +const styleSheet = theme => ({ + mainText: { + color: theme.palette.grey[300], + fontSize: 12, + padding: '4px 8px', + }, + text: { + color: theme.palette.grey[400], + whiteSpace: 'pre-line', + paddingLeft: 16, + paddingBottom: 8, + fontSize: 12, + }, + divider: { + background: theme.palette.grey[400], + }, +}); + +const messages = { + info: 'Number of Reviews Completed / Number of Reviewers', +}; + +const renderExpandedCell = classes => ( + + {messages.info} + +); + + +const EoiSectorCell = (props) => { + const { classes, id, reviews } = props; + return ( + +
+ {reviews} +
+
+ ); +}; + +EoiSectorCell.propTypes = { + classes: PropTypes.object.isRequired, + reviews: PropTypes.String, + id: PropTypes.number.isRequired, +}; + +export default withStyles(styleSheet, { name: 'EoiSectorCell' })(EoiSectorCell); diff --git a/frontend/src/components/eois/details/applications/openCfeiPreselected.js b/frontend/src/components/eois/details/applications/openCfeiPreselected.js index 176e04a4b..332195c8e 100644 --- a/frontend/src/components/eois/details/applications/openCfeiPreselected.js +++ b/frontend/src/components/eois/details/applications/openCfeiPreselected.js @@ -12,6 +12,7 @@ import WithGreyColor from '../../../common/hoc/withGreyButtonStyle'; import Compare from '../../buttons/compareButton'; import PreselectedTotalScore from '../../cells/preselectedTotalScore'; import PreselectedYourScore from '../../cells/preselectedYourScore'; +import PreselectedReviewsCell from '../../cells/preselectedReviewsCell'; import { loadApplications } from '../../../../reducers/partnersApplicationsList'; import { APPLICATION_STATUSES } from '../../../../helpers/constants'; import { isQueryChanged } from '../../../../helpers/apiHelper'; @@ -82,6 +83,11 @@ class OpenCfeiPreselections extends Component { hovered={hovered} allowedToEdit={this.props.allowedToEdit} />); + } else if (column.name === 'reviews') { + + return (); } return undefined; } diff --git a/frontend/src/reducers/partnersApplicationsList.js b/frontend/src/reducers/partnersApplicationsList.js index 515641187..90fce92c5 100644 --- a/frontend/src/reducers/partnersApplicationsList.js +++ b/frontend/src/reducers/partnersApplicationsList.js @@ -17,6 +17,7 @@ const initialState = { { name: 'type_org', title: 'Type of Organization' }, { name: 'id', title: 'Application ID' }, { name: 'status', title: 'Status', width: 300 }, + { name: 'review_progress', title: 'Status', width: 300 }, ], applications: [], itemsCount: 0, From 07101095ebc21d1f6f5d4afecfb3bc63bfadd221 Mon Sep 17 00:00:00 2001 From: Adam Podsiadlo Date: Mon, 29 Jan 2018 11:01:25 +0100 Subject: [PATCH 12/19] remove is active status --- .../notes/partnerApplicationsNotesFilter.js | 46 ++----------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/applications/notes/partnerApplicationsNotesFilter.js b/frontend/src/components/applications/notes/partnerApplicationsNotesFilter.js index 0ee3d4c63..7905bcaf8 100644 --- a/frontend/src/components/applications/notes/partnerApplicationsNotesFilter.js +++ b/frontend/src/components/applications/notes/partnerApplicationsNotesFilter.js @@ -8,16 +8,13 @@ import { withStyles } from 'material-ui/styles'; import Grid from 'material-ui/Grid'; import Button from 'material-ui/Button'; import SelectForm from '../../forms/selectForm'; -import RadioForm from '../../forms/radioForm'; import TextFieldForm from '../../forms/textFieldForm'; import Agencies from '../../forms/fields/projectFields/agencies'; import AdminOneLocation from '../../forms/fields/projectFields/adminOneLocations'; import CountryField from '../../forms/fields/projectFields/locationField/countryField'; import { selectMappedSpecializations, - selectNormalizedCountries, selectNormalizedExtendedApplicationStatuses } from '../../../store'; import resetChanges from '../../eois/filters/eoiHelper'; -import { APPLICATION_STATUSES } from '../../../helpers/constants'; const messages = { choose: 'Choose', @@ -45,17 +42,6 @@ const styleSheet = theme => ({ }, }); -export const STATUS_VAL = [ - { - value: true, - label: 'Active', - }, - { - value: false, - label: 'Completed', - }, -]; - class PartnerApplicationsNotesFilter extends Component { constructor(props) { super(props); @@ -71,13 +57,9 @@ class PartnerApplicationsNotesFilter extends Component { const { pathName, query } = this.props; resetChanges(pathName, query); - const active = !!(this.props.query.cfei_active === 'true' || (typeof (this.props.query.cfei_active) === 'boolean' && this.props.query.cfei_active) || !this.props.query.cfei_active); - history.push({ pathname: pathName, - query: R.merge(query, - { cfei_active: active }, - ), + query, }); } @@ -85,12 +67,9 @@ class PartnerApplicationsNotesFilter extends Component { if (R.isEmpty(nextProps.query)) { const { pathname } = nextProps.location; - const active = this.props.query.cfei_active ? this.props.query.cfei_active : true; history.push({ pathname, - query: R.merge(this.props.query, - { cfei_active: active }, - ), + query: this.props.query, }); } } @@ -100,7 +79,7 @@ class PartnerApplicationsNotesFilter extends Component { const { pathName, query } = this.props; const { project_title, agency, country_code, specializations, - posted_from_date, posted_to_date, cfei_active, applications_status, locations } = values; + posted_from_date, posted_to_date, applications_status, locations } = values; history.push({ pathname: pathName, @@ -109,7 +88,6 @@ class PartnerApplicationsNotesFilter extends Component { project_title, agency, applications_status, - cfei_active, country_code, specializations: Array.isArray(specializations) ? specializations.join(',') : specializations, posted_from_date, @@ -127,14 +105,12 @@ class PartnerApplicationsNotesFilter extends Component { history.push({ pathname: pathName, - query: R.merge(query, - { cfei_active: true }, - ), + query, }); } render() { - const { classes, countryCode, countries, specs, handleSubmit, cnStatus, reset } = this.props; + const { classes, countryCode, specs, handleSubmit, cnStatus, reset } = this.props; return ( @@ -178,14 +154,6 @@ class PartnerApplicationsNotesFilter extends Component { optional />
- - - { const { query: { project_title } = {} } = ownProps.location; const { query: { country_code } = {} } = ownProps.location; const { query: { agency } = {} } = ownProps.location; - const { query: { cfei_active } = {} } = ownProps.location; const { query: { applications_status } = {} } = ownProps.location; const { query: { locations } = {} } = ownProps.location; const { query: { specializations } = {} } = ownProps.location; @@ -260,7 +226,6 @@ const mapStateToProps = (state, ownProps) => { R.map(Number, specializations.split(',')); return { - countries: selectNormalizedCountries(state), specs: selectMappedSpecializations(state), cnStatus: selectNormalizedExtendedApplicationStatuses(state), pathName: ownProps.location.pathname, @@ -270,7 +235,6 @@ const mapStateToProps = (state, ownProps) => { project_title, country_code, agency: agencyQ, - cfei_active, applications_status, locations, specializations: specializationsQ, From fe95a97452b11928ba608954ea1d681097139497 Mon Sep 17 00:00:00 2001 From: Adam Podsiadlo Date: Mon, 29 Jan 2018 11:18:22 +0100 Subject: [PATCH 13/19] add number of applications to table --- .../dashboard/agencyDashboard/listOfConceptNotesTable.js | 1 + frontend/src/reducers/applicationsToScore.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/dashboard/agencyDashboard/listOfConceptNotesTable.js b/frontend/src/components/dashboard/agencyDashboard/listOfConceptNotesTable.js index 94192cdc2..c8312a982 100644 --- a/frontend/src/components/dashboard/agencyDashboard/listOfConceptNotesTable.js +++ b/frontend/src/components/dashboard/agencyDashboard/listOfConceptNotesTable.js @@ -13,6 +13,7 @@ import { formatDateForPrint } from '../../../helpers/dates'; const columns = [ { name: 'title', title: 'Project Title' }, { name: 'id', title: 'CFEI' }, + { name: 'eoi_applications_count', title: 'Number of Applications' }, { name: 'deadline_date', title: 'Notification of results deadline' }, ]; diff --git a/frontend/src/reducers/applicationsToScore.js b/frontend/src/reducers/applicationsToScore.js index 5301bb17a..b705d7407 100644 --- a/frontend/src/reducers/applicationsToScore.js +++ b/frontend/src/reducers/applicationsToScore.js @@ -33,10 +33,11 @@ export const saveApplications = (action) => { title, id, deadline_date, - } }) => ({ + }, eoi_applications_count }) => ({ title, id, deadline_date, + eoi_applications_count, })); return { applications: newApplications, count }; }; From 2006a0dc99236d1f2f5bbfe657997bc577f54ae3 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Mon, 29 Jan 2018 11:31:10 +0100 Subject: [PATCH 14/19] Fixed specializations filter to work for both cases --- backend/unpp_api/apps/project/filters.py | 131 ++++++++++++----------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/backend/unpp_api/apps/project/filters.py b/backend/unpp_api/apps/project/filters.py index 7918a3b14..1ad69313b 100644 --- a/backend/unpp_api/apps/project/filters.py +++ b/backend/unpp_api/apps/project/filters.py @@ -12,12 +12,13 @@ class BaseProjectFilter(django_filters.FilterSet): - title = CharFilter(method='get_title') - country_code = CharFilter(method='get_country_code') - locations = CharFilter(method='get_locations') - specializations = ModelMultipleChoiceFilter(widget=CSVWidget(), - queryset=Specialization.objects.all()) - active = BooleanFilter(method='get_active', widget=BooleanWidget()) + title = CharFilter(method='filter_title') + country_code = CharFilter(method='filter_country_code') + locations = CharFilter(method='filter_locations') + specializations = ModelMultipleChoiceFilter( + widget=CSVWidget(), queryset=Specialization.objects.all() + ) + active = BooleanFilter(method='filter_active', widget=BooleanWidget()) posted_from_date = DateFilter(name='created', lookup_expr='date__gte') posted_to_date = DateFilter(name='created', lookup_expr='date__lte') selected_source = CharFilter(lookup_expr='iexact') @@ -26,16 +27,16 @@ class Meta: model = EOI fields = ['title', 'country_code', 'locations', 'specializations', 'agency', 'active', 'selected_source'] - def get_title(self, queryset, name, value): + def filter_title(self, queryset, name, value): return queryset.filter(title__icontains=value) - def get_country_code(self, queryset, name, value): + def filter_country_code(self, queryset, name, value): return queryset.filter(locations__admin_level_1__country_code=(value and value.upper())) - def get_locations(self, queryset, name, value): + def filter_locations(self, queryset, name, value): return queryset.filter(locations__admin_level_1=value) - def get_active(self, queryset, name, value): + def filter_active(self, queryset, name, value): if value: return queryset.filter(is_completed=False) return queryset.filter(is_completed=True) @@ -43,19 +44,19 @@ def get_active(self, queryset, name, value): class ApplicationsFilter(django_filters.FilterSet): - project_title = CharFilter(method='get_project_title') - legal_name = CharFilter(method='get_legal_name') - country_code = CharFilter(method='get_country_code') - location = CharFilter(method='get_location') - specializations = ModelMultipleChoiceFilter(widget=CSVWidget(), - name='eoi__specializations', - queryset=Specialization.objects.all()) - year = CharFilter(method='get_year') - concern = CharFilter(method='get_concern') - status = CharFilter(method='get_status') - agency = CharFilter(method='get_agency') + project_title = CharFilter(method='filter_project_title') + legal_name = CharFilter(method='filter_legal_name') + country_code = CharFilter(method='filter_country_code') + location = CharFilter(method='filter_location') + specializations = ModelMultipleChoiceFilter( + widget=CSVWidget(), name='eoi__specializations', queryset=Specialization.objects.all() + ) + year = CharFilter(method='filter_year') + concern = CharFilter(method='filter_concern') + status = CharFilter(method='filter_status') + agency = CharFilter(method='filter_agency') did_win = BooleanFilter(widget=BooleanWidget()) - cfei_active = BooleanFilter(method='get_cfei_active', widget=BooleanWidget()) + cfei_active = BooleanFilter(method='filter_cfei_active', widget=BooleanWidget()) applications_status = ChoiceFilter(method='filter_applications_status', choices=EXTENDED_APPLICATION_STATUSES) @@ -63,25 +64,25 @@ class Meta: model = Application fields = ['project_title', 'legal_name', 'country_code', 'eoi', 'partner', 'status', 'did_win'] - def get_project_title(self, queryset, name, value): + def filter_project_title(self, queryset, name, value): return queryset.filter(eoi__title__icontains=value) - def get_legal_name(self, queryset, name, value): + def filter_legal_name(self, queryset, name, value): return queryset.filter(partner__legal_name__icontains=value) - def get_country_code(self, queryset, name, value): - return queryset.filter(eoi__locations__admin_level_1__country_code=(value and value.upper())) + def filter_country_code(self, queryset, name, value): + return queryset.filter(eoi__locations__admin_level_1__country_code__iexact=value) - def get_location(self, queryset, name, value): + def filter_location(self, queryset, name, value): return queryset.filter(eoi__locations__admin_level_1=value) - def get_year(self, queryset, name, value): + def filter_year(self, queryset, name, value): return queryset.filter(partner__experiences__years=value) - def get_concern(self, queryset, name, value): + def filter_concern(self, queryset, name, value): return queryset.filter(partner__mandate_mission__concern_groups__contains=[value]) - def get_status(self, queryset, name, value): + def filter_status(self, queryset, name, value): return queryset.filter(status=value) def filter_applications_status(self, queryset, name, value): @@ -119,87 +120,93 @@ def filter_applications_status(self, queryset, name, value): return queryset.filter(**filters.get(value, {})) - def get_agency(self, queryset, name, value): + def filter_agency(self, queryset, name, value): return queryset.filter(eoi__agency=value) - def get_cfei_active(self, queryset, name, value): + def filter_cfei_active(self, queryset, name, value): if value: return queryset.filter(eoi__is_completed=False) return queryset.filter(eoi__is_completed=True) - def get_type_of_org(self, queryset, name, value): - return queryset.filter(partner__display_type__icontains=value) - class ApplicationsEOIFilter(django_filters.FilterSet): - legal_name = CharFilter(method='get_legal_name') - country_code = CharFilter(method='get_country_code') - location = CharFilter(method='get_location') - specializations = ModelMultipleChoiceFilter(widget=CSVWidget(), - name='partner__experiences__specialization', - queryset=Specialization.objects.all()) - concern = CharFilter(method='get_concern') - type_of_org = CharFilter(method='get_type_of_org') - status = CharFilter(method='get_status') + legal_name = CharFilter(method='filter_legal_name') + country_code = CharFilter(method='filter_country_code') + location = CharFilter(method='filter_location') + specializations = ModelMultipleChoiceFilter( + widget=CSVWidget(), name='partner__experiences__specialization', queryset=Specialization.objects.all() + ) + concern = CharFilter(method='filter_concern') + type_of_org = CharFilter(method='filter_type_of_org') + status = CharFilter(method='filter_status') class Meta: model = Application fields = ['legal_name', 'country_code', 'location', 'specializations', 'concern', 'type_of_org'] - def get_legal_name(self, queryset, name, value): + def filter_legal_name(self, queryset, name, value): return queryset.filter(partner__legal_name__icontains=value) - def get_country_code(self, queryset, name, value): - return queryset.filter(partner__country_code=(value and value.upper())) + def filter_country_code(self, queryset, name, value): + return queryset.filter(partner__country_code__iexact=value) - def get_location(self, queryset, name, value): + def filter_location(self, queryset, name, value): return queryset.filter(partner__country_presence__contains=[value and value.upper()]) - def get_concern(self, queryset, name, value): + def filter_concern(self, queryset, name, value): return queryset.filter(partner__mandate_mission__concern_groups__contains=[value]) - def get_type_of_org(self, queryset, name, value): + def filter_type_of_org(self, queryset, name, value): return queryset.filter(partner__display_type__icontains=value) - def get_status(self, queryset, name, value): + def filter_status(self, queryset, name, value): return queryset.filter(status=value) class ApplicationsUnsolicitedFilter(django_filters.FilterSet): - project_title = CharFilter(method='get_project_title') - country_code = CharFilter(method='get_country_code') - location = CharFilter(method='get_location') + project_title = CharFilter(method='filter_project_title') + country_code = CharFilter(method='filter_country_code') + location = CharFilter(method='filter_locations') specializations = ModelMultipleChoiceFilter( - widget=CSVWidget(), name='eoi__specializations', queryset=Specialization.objects.all() + widget=CSVWidget(), method='filter_specializations', queryset=Specialization.objects.all() ) - agency = CharFilter(method='get_agency') - ds_converted = BooleanFilter(method='get_ds_converted', widget=BooleanWidget()) + agency = CharFilter(method='filter_agency') + ds_converted = BooleanFilter(method='filter_ds_converted', widget=BooleanWidget()) class Meta: model = Application fields = ['project_title', 'country_code', 'location', 'specializations', 'agency'] - def get_project_title(self, queryset, name, value): + def filter_project_title(self, queryset, name, value): return queryset.filter( Q(proposal_of_eoi_details__title__icontains=value) | # unsolicited Q(eoi__title__icontains=value) # direct selection - developed from unsolicited ) - def get_country_code(self, queryset, name, value): + def filter_country_code(self, queryset, name, value): return queryset.filter( Q(locations_proposal_of_eoi__admin_level_1__country_code=value) | # unsolicited Q(eoi__locations__admin_level_1__country_code=value) # direct selection - developed from unsolicited ) - def get_locations(self, queryset, name, value): + def filter_locations(self, queryset, name, value): return queryset.filter( Q(locations_proposal_of_eoi__admin_level_1=[value]) | # unsolicited Q(eoi__locations__admin_level_1=[value]) # direct selection - developed from unsolicited ) - def get_agency(self, queryset, name, value): + def filter_agency(self, queryset, name, value): return queryset.filter(agency=value) - def get_ds_converted(self, queryset, name, value): + def filter_ds_converted(self, queryset, name, value): return queryset.filter(eoi_converted__isnull=(not value)) + + def filter_specializations(self, queryset, name, value): + if value: + query = Q(eoi__specializations=value) + value = list(value.values_list('id', flat=True)) + for pk in value: + query |= Q(proposal_of_eoi_details__specializations__contains=pk) + return queryset.filter(query) + return queryset From 6bd13f3ebcbe21cab0b626d61675b19a5e53934a Mon Sep 17 00:00:00 2001 From: Adam Podsiadlo Date: Mon, 29 Jan 2018 14:05:57 +0100 Subject: [PATCH 15/19] refresh partner profile last update date --- frontend/src/reducers/partnerProfileDetailsUpdate.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/reducers/partnerProfileDetailsUpdate.js b/frontend/src/reducers/partnerProfileDetailsUpdate.js index e87535e63..7e0c8935d 100644 --- a/frontend/src/reducers/partnerProfileDetailsUpdate.js +++ b/frontend/src/reducers/partnerProfileDetailsUpdate.js @@ -1,5 +1,6 @@ import R from 'ramda'; import { patchPartnerProfileTab } from '../helpers/api/api'; +import { sessionChange } from './session'; export const PATCH_DETAILS_STARTED = 'PATCH_DETAILS_STARTED'; export const PATCH_DETAILS_ENDED = 'PATCH_DETAILS_ENDED'; @@ -31,10 +32,13 @@ export const patchDetailsSuccess = (partnerDetails, getState) => ({ type: PATCH_DETAILS_SUCCESS, partnerDetails, getState }); export const patchDetailsFailure = error => ({ type: PATCH_DETAILS_FAILURE, error }); -export const patchPartnerProfile = (partnerId, tabName, body) => (dispatch) => { +export const patchPartnerProfile = (partnerId, tabName, body) => (dispatch, getState) => { + const session = getState().session; + dispatch(patchDetailsStarted()); - return patchPartnerProfileTab(partnerId, tabName, body); + return patchPartnerProfileTab(partnerId, tabName, body) + .then(() => dispatch(sessionChange(R.assoc('lastUpdate', new Date(), session)))); }; export default function partnerProfileDetailsStatus(state = initialState, action) { From b71856cdf32240d2608a4a24dd7c52647e75c71e Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Mon, 29 Jan 2018 14:19:18 +0100 Subject: [PATCH 16/19] removed not working cloud commands, add preview uwsgi logs command --- fabfile.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/fabfile.py b/fabfile.py index bd75e207d..94ec3ce3b 100644 --- a/fabfile.py +++ b/fabfile.py @@ -64,6 +64,11 @@ def managepy(command=''): local(cmd) +def preview_uwsgi_log(): + cmd = 'docker-compose exec backend tail -f /var/log/uwsgi_global.log' + local(cmd) + + def fakedata(quantity=50, clean_before=True): """ Load example data from fakedata management command. @@ -107,34 +112,6 @@ def pep8(): local('docker-compose exec backend flake8 ./ --count') -def cloud_login(): - """ - Login into docker cloud services - """ - local('docker login') - - -def cloud_setup(namespace): - """ - Set docker cloud namespace - """ - local('export DOCKERCLOUD_NAMESPACE={}'.format(namespace)) - - -def cloud_list(): - """ - List docker cloud containers - """ - local('docker-cloud container ps') - - -def cloud_ssh(container_uuid): - """ - SSH into a cloud container - """ - local('docker-cloud container exec {} /bin/bash'.format(container_uuid)) - - def make_admin(): """ Create admin user for the backend From 6628b137ad6d1f90bde1d2afb0a5f69f373b7216 Mon Sep 17 00:00:00 2001 From: Maciej Jaworski Date: Mon, 29 Jan 2018 14:26:43 +0100 Subject: [PATCH 17/19] fix missing blank on a field --- .../migrations/0056_auto_20180129_1327.py | 21 +++++++++++++++++++ backend/unpp_api/apps/partner/models.py | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 backend/unpp_api/apps/partner/migrations/0056_auto_20180129_1327.py diff --git a/backend/unpp_api/apps/partner/migrations/0056_auto_20180129_1327.py b/backend/unpp_api/apps/partner/migrations/0056_auto_20180129_1327.py new file mode 100644 index 000000000..97c84bb66 --- /dev/null +++ b/backend/unpp_api/apps/partner/migrations/0056_auto_20180129_1327.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.8 on 2018-01-29 13:27 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partner', '0055_merge_20180123_1153'), + ] + + operations = [ + migrations.AlterField( + model_name='partner', + name='country_presence', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('AF', 'Afghanistan'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AZ', 'Azerbaijan'), ('AI', 'Anguilla'), ('AG', 'Antigua and Barbuda'), ('BB', 'Barbados'), ('VG', 'Virgin Islands (UK)'), ('DM', 'Dominica'), ('GD', 'Grenada'), ('MS', 'Montserrat'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('VC', 'Saint Vincent and the Grenadines'), ('TC', 'Turks & Caicos'), ('TT', 'Trinidad and Tobago'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BW', 'Botswana'), ('BA', 'Bosnia and Herzegovina'), ('BR', 'Brazil'), ('BG', 'Bulgaria'), ('MM', 'Myanmar'), ('BI', 'Burundi'), ('BY', 'Belarus'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CF', 'Central African Republic'), ('LK', 'Sri Lanka'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CO', 'Colombia'), ('CD', 'Congo, Dem. Rep.'), ('CR', 'Costa Rica'), ('HR', 'Croatia'), ('CU', 'Cuba'), ('BJ', 'Benin'), ('DK', 'Denmark'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ET', 'Ethiopia'), ('ER', 'Eritrea'), ('AS', 'American Samoa'), ('CK', 'Cook Islands'), ('FJ', 'Fiji'), ('KI', 'Kiribati'), ('MH', 'Marshall Islands'), ('FM', 'Micronesia'), ('NR', 'Nauru'), ('NU', 'Niue'), ('PW', 'Palau, Republic Of'), ('WS', 'Samoa'), ('SB', 'Solomon Islands'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TV', 'Tuvalu'), ('VU', 'Vanuatu'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('GH', 'Ghana'), ('GT', 'Guatemala'), ('GN', 'Guinea'), ('GY', 'Guyana'), ('SR', 'Suriname'), ('HT', 'Haiti'), ('HN', 'Honduras'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran'), ('IQ', 'Iraq'), ('CI', "Cote D'Ivoire"), ('JM', 'Jamaica'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KG', 'Kyrgyzstan'), ('LA', "Lao, People's Dem. Rep"), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libya'), ('MK', 'Macedonia, TFYR'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MR', 'Mauritania'), ('MX', 'Mexico'), ('MN', 'Mongolia'), ('MA', 'Morocco'), ('NP', 'Nepal'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('PK', 'Pakistan'), ('PA', 'Panama'), ('PY', 'Paraguay'), ('CG', 'Congo'), ('PE', 'Peru'), ('PH', 'Philippines'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BH', 'Bahrain'), ('KW', 'Kuwait'), ('QA', 'Qatar'), ('SA', 'Saudi Arabia'), ('AE', 'United Arab Emirates'), ('SN', 'Senegal'), ('SL', 'Sierra Leone'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('SD', 'Sudan'), ('SZ', 'Swaziland'), ('SS', 'South Sudan'), ('SY', 'Syrian Arab Republic'), ('TJ', 'Tajikistan'), ('TH', 'Thailand'), ('TG', 'Togo'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('EG', 'Egypt'), ('TZ', 'Tanzania, United Republic of'), ('BF', 'Burkina Faso'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VE', 'Venezuela'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('BD', 'Bangladesh'), ('KP', "Korea, Democratic People's Republic of"), ('VN', 'Viet Nam'), ('MD', 'Moldova'), ('CH', 'Switzerland'), ('BZ', 'Belize'), ('ZW', 'Zimbabwe'), ('OM', 'Oman'), ('PG', 'Papua New Guinea'), ('KM', 'Comoros'), ('DJ', 'Djibouti'), ('AO', 'Angola'), ('CV', 'Cabo Verde'), ('ST', 'Sao Tome and Principe'), ('GW', 'Guinea-Bissau'), ('MZ', 'Mozambique'), ('NA', 'Namibia'), ('PS', 'Palestine'), ('TL', 'Timor-Leste'), ('ME', 'Montenegro'), ('RS', 'Serbia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('BE', 'Belgium'), ('BM', 'Bermuda'), ('BS', 'Bahamas'), ('BN', 'Brunei'), ('CA', 'Canada'), ('KY', 'Cayman Islands'), ('CZ', 'Czech Republic'), ('CY', 'Cyprus'), ('EE', 'Estonia'), ('FI', 'Finland'), ('FR', 'France'), ('DE', 'Germany'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GU', 'Guam'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IE', 'Ireland'), ('IL', 'Israel'), ('IT', 'Italy'), ('JP', 'Japan'), ('LV', 'Latvia'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MU', 'Mauritius'), ('MT', 'Malta'), ('MC', 'Monaco'), ('NC', 'New Caledonia'), ('NL', 'Netherlands'), ('NO', 'Norway'), ('NZ', 'New Zealand'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('KR', 'Korea, Republic of'), ('SC', 'Seychelles'), ('SG', 'Singapore'), ('SK', 'Slovakia'), ('ES', 'Spain'), ('SI', 'Slovenia'), ('SE', 'Sweden'), ('GB', 'United Kingdom'), ('US', 'United States of America'), ('VI', 'Virgin Islands (USA)'), ('AD', 'Andorra'), ('GF', 'French Guiana'), ('PF', 'French Polynesia (France)'), ('GL', 'Greenland'), ('GP', 'Guadeloupe'), ('LI', 'Liechtenstein'), ('MQ', 'Martinique'), ('AN', 'Netherlands Antilles'), ('MP', 'Northern Mariana Islands'), ('RE', 'Reunion'), ('SH', 'Saint Helena'), ('SM', 'San Marino'), ('CS', 'Serbia & Montenegro'), ('VA', 'Vatican City'), ('WF', 'Wallis & Futuna Islands')], max_length=2), blank=True, default=list, null=True, size=None), + ), + ] diff --git a/backend/unpp_api/apps/partner/models.py b/backend/unpp_api/apps/partner/models.py index 835ab8707..7b0e3e1dd 100644 --- a/backend/unpp_api/apps/partner/models.py +++ b/backend/unpp_api/apps/partner/models.py @@ -55,7 +55,8 @@ class Partner(TimeStampedModel): country_presence = ArrayField( models.CharField(max_length=2, choices=COUNTRIES_ALPHA2_CODE), default=list, - null=True + null=True, + blank=True, ) staff_globally = models.CharField(max_length=3, choices=STAFF_GLOBALLY_CHOICES, null=True, blank=True) # country profile information From a46feb775d6ebe3cd62dbf82bd3913f3591626c4 Mon Sep 17 00:00:00 2001 From: Adam Podsiadlo Date: Mon, 29 Jan 2018 14:45:25 +0100 Subject: [PATCH 18/19] update review tooltip in preselected list --- .../eois/cells/preselectedReviewsCell.js | 31 +++++++------------ .../applications/openCfeiPreselected.js | 6 ++-- .../src/reducers/partnersApplicationsList.js | 1 - 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/eois/cells/preselectedReviewsCell.js b/frontend/src/components/eois/cells/preselectedReviewsCell.js index 571fd2ffb..94193708f 100644 --- a/frontend/src/components/eois/cells/preselectedReviewsCell.js +++ b/frontend/src/components/eois/cells/preselectedReviewsCell.js @@ -1,6 +1,7 @@ import React from 'react'; import R from 'ramda'; import PropTypes from 'prop-types'; +import { TableCell } from 'material-ui/Table'; import { withStyles } from 'material-ui/styles'; import Typography from 'material-ui/Typography'; import Tooltip from '../../common/portalTooltip'; @@ -11,16 +12,6 @@ const styleSheet = theme => ({ fontSize: 12, padding: '4px 8px', }, - text: { - color: theme.palette.grey[400], - whiteSpace: 'pre-line', - paddingLeft: 16, - paddingBottom: 8, - fontSize: 12, - }, - divider: { - background: theme.palette.grey[400], - }, }); const messages = { @@ -31,7 +22,7 @@ const renderExpandedCell = classes => ( {messages.info} @@ -41,14 +32,16 @@ const renderExpandedCell = classes => ( const EoiSectorCell = (props) => { const { classes, id, reviews } = props; return ( - -
- {reviews} -
-
+ + +
+ {reviews} +
+
+
); }; diff --git a/frontend/src/components/eois/details/applications/openCfeiPreselected.js b/frontend/src/components/eois/details/applications/openCfeiPreselected.js index 332195c8e..0d9c71657 100644 --- a/frontend/src/components/eois/details/applications/openCfeiPreselected.js +++ b/frontend/src/components/eois/details/applications/openCfeiPreselected.js @@ -83,10 +83,10 @@ class OpenCfeiPreselections extends Component { hovered={hovered} allowedToEdit={this.props.allowedToEdit} />); - } else if (column.name === 'reviews') { - + } else if (column.name === 'review_progress') { return (); } return undefined; diff --git a/frontend/src/reducers/partnersApplicationsList.js b/frontend/src/reducers/partnersApplicationsList.js index 90fce92c5..515641187 100644 --- a/frontend/src/reducers/partnersApplicationsList.js +++ b/frontend/src/reducers/partnersApplicationsList.js @@ -17,7 +17,6 @@ const initialState = { { name: 'type_org', title: 'Type of Organization' }, { name: 'id', title: 'Application ID' }, { name: 'status', title: 'Status', width: 300 }, - { name: 'review_progress', title: 'Status', width: 300 }, ], applications: [], itemsCount: 0, From cc811181b62261cbc98a318b958b49770fff3249 Mon Sep 17 00:00:00 2001 From: marcindo Date: Mon, 29 Jan 2018 17:17:21 +0100 Subject: [PATCH 19/19] change edit review button to be always static --- .../reviewContent/editReviewModalButton.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/components/eois/details/applications/applicationSummary/reviewContent/editReviewModalButton.js b/frontend/src/components/eois/details/applications/applicationSummary/reviewContent/editReviewModalButton.js index 78e679d08..7ef298901 100644 --- a/frontend/src/components/eois/details/applications/applicationSummary/reviewContent/editReviewModalButton.js +++ b/frontend/src/components/eois/details/applications/applicationSummary/reviewContent/editReviewModalButton.js @@ -20,10 +20,7 @@ const styleSheet = () => ({ height: 20, }, editIcon: { - fill: '#FFFFFF', - '&:hover': { - fill: '#8B8C8D', - }, + fill: '#8B8C8D', }, });