Skip to content

Commit 06cf78f

Browse files
authored
Merge pull request #4065 from unicef/develop
Develop
2 parents 064dd88 + 62de504 commit 06cf78f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2920
-215
lines changed

screenshots/PCA_process.jpg

-170 KB
Binary file not shown.

screenshots/equitrack_map.png

-469 KB
Binary file not shown.

screenshots/equitrack_pcas.png

-344 KB
Binary file not shown.

src/etools/applications/core/data/users.json

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/etools/applications/core/middleware.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import threading
23

34
from django.conf import settings
45
from django.contrib.contenttypes.models import ContentType
@@ -28,6 +29,8 @@
2829

2930
INACTIVE_WORKSPACE_URL = reverse('workspace-inactive')
3031

32+
_thread_locals = threading.local()
33+
3134

3235
class QueryCountDebugMiddleware(MiddlewareMixin):
3336
""" Debug db connections"""
@@ -153,13 +156,28 @@ def __init__(self, get_response):
153156

154157
def __call__(self, request):
155158
# Check if the request method is not GET
159+
if request.user.is_authenticated:
160+
_thread_locals.user = request.user
156161
if request.user.is_authenticated and request.user.is_unicef_user():
157-
return self.get_response(request)
162+
try:
163+
response = self.get_response(request)
164+
return response
165+
finally:
166+
_thread_locals.user = None
158167

159168
if request.user.is_authenticated:
160169
# check where they're trying to access:
161170
if any(request.path.startswith(path) for path in settings.PARTNER_PROTECTED_URLS):
162171
user_group_names = [g.name for g in request.user.groups]
163172
if not any([g in PARTNER_PD_ACTIVE_GROUPS for g in user_group_names]):
164173
return HttpResponseForbidden("You don't have permission to perform this action.")
165-
return self.get_response(request)
174+
try:
175+
response = self.get_response(request)
176+
return response
177+
finally:
178+
_thread_locals.user = None
179+
180+
181+
def get_current_user():
182+
user = getattr(_thread_locals, 'user', None)
183+
return user if user else None

src/etools/applications/field_monitoring/fm_settings/tests/test_views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ def test_get_path(self):
128128
self.assertEqual(response.data[0]['id'], str(self.country.id))
129129
self.assertEqual(response.data[1]['id'], str(self.child_location.id))
130130

131+
def test_get_path_inactive_location(self):
132+
"""Path endpoint should be accessible for inactive locations and return only active ancestors."""
133+
inactive_child = LocationFactory(parent=self.country, is_active=False)
134+
135+
response = self.forced_auth_req(
136+
'get', reverse('field_monitoring_settings:locations-path', args=[inactive_child.id]),
137+
user=self.unicef_user,
138+
)
139+
140+
self.assertEqual(response.status_code, status.HTTP_200_OK)
141+
# Inactive location itself is excluded; only active ancestors should be returned
142+
self.assertEqual(len(response.data), 1)
143+
self.assertEqual(response.data[0]['id'], str(self.country.id))
144+
131145

132146
class LocationSitesViewTestCase(TestExportMixin, FMBaseTestCaseMixin, BaseTenantTestCase):
133147
def setUp(self):

src/etools/applications/field_monitoring/fm_settings/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ class FMLocationsViewSet(FMBaseViewSet, mixins.ListModelMixin, viewsets.GenericV
156156

157157
@action(methods=['get'], detail=True)
158158
def path(self, request, *args, **kwargs):
159-
ancestors = self.get_object().get_ancestors(include_self=True).filter(is_active=True)
159+
location = get_object_or_404(Location, pk=kwargs.get('pk'))
160+
ancestors = location.get_ancestors(include_self=True).filter(is_active=True)
160161
return Response(data=self.get_serializer(instance=ancestors, many=True).data)
161162

162163

src/etools/applications/field_monitoring/planning/activity_validation/permissions.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,19 @@ def is_ma_user():
4141
return is_visit_lead() or is_team_member()
4242

4343
monitor_types = self.instance.MONITOR_TYPE_CHOICES
44+
is_staff_visit = self.instance.monitor_type in [monitor_types.staff, monitor_types.both]
45+
is_tpm_visit = self.instance.monitor_type in [monitor_types.tpm, monitor_types.both]
46+
visit_lead = is_visit_lead()
47+
ma_user = is_ma_user()
4448

4549
self.condition_map = {
46-
'is_ma_related_user': is_ma_user(),
47-
'is_visit_lead': is_visit_lead(),
48-
'tpm_visit': self.instance.monitor_type == monitor_types.tpm,
49-
'staff_visit': self.instance.monitor_type == monitor_types.staff,
50-
'staff_visit+is_visit_lead': self.instance.monitor_type == monitor_types.staff and is_visit_lead(),
51-
'tpm_visit+tpm_ma_related': self.instance.monitor_type == monitor_types.tpm and is_ma_user(),
52-
'tpm_visit+tpm_ma_related+is_visit_lead': (self.instance.monitor_type == monitor_types.tpm and is_ma_user()) or is_visit_lead(),
50+
'is_ma_related_user': ma_user,
51+
'is_visit_lead': visit_lead,
52+
'tpm_visit': is_tpm_visit,
53+
'staff_visit': is_staff_visit,
54+
'staff_visit+is_visit_lead': is_staff_visit and visit_lead,
55+
'tpm_visit+tpm_ma_related': is_tpm_visit and ma_user,
56+
'tpm_visit+tpm_ma_related+is_visit_lead': (is_tpm_visit and ma_user) or visit_lead,
5357
}
5458

5559
if getattr(self.instance, 'old', None) is not None:

src/etools/applications/field_monitoring/planning/activity_validation/validations/basic.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from django.db import connection
12
from django.utils.translation import gettext as _
23

34
from etools_validator.exceptions import BasicValidationError
45

56
from etools.applications.field_monitoring.planning.models import MonitoringActivity
67
from etools.applications.partners.models import PartnerOrganization
78
from etools.applications.reports.models import CountryProgramme, Result
9+
from etools.applications.users.models import Realm
810

911

1012
def staff_activity_has_no_tpm_partner(i):
@@ -14,10 +16,12 @@ def staff_activity_has_no_tpm_partner(i):
1416

1517

1618
def tpm_staff_members_belongs_to_the_partner(i):
17-
if not i.tpm_partner:
19+
# For BOTH we do not restrict TPM team members to the selected TPM partner
20+
if not i.tpm_partner or i.monitor_type == MonitoringActivity.MONITOR_TYPE_CHOICES.both:
1821
return True
1922

20-
team_members = set([tm.id for tm in i.team_members.all()])
23+
team_members_qs = list(i.team_members.all())
24+
team_members = set([tm.id for tm in team_members_qs])
2125
if i.old_instance:
2226
old_team_members = set([tm.id for tm in i.old_instance.team_members.all()])
2327
members_to_validate = team_members - old_team_members
@@ -63,3 +67,22 @@ def interventions_connected_with_cp_outputs(i):
6367
raise BasicValidationError(error_text % ', '.join(wrong_cp_outputs))
6468

6569
return True
70+
71+
72+
def assignees_have_active_access(i):
73+
if i.monitor_type != MonitoringActivity.MONITOR_TYPE_CHOICES.both:
74+
return True
75+
76+
def has_active_realm(user):
77+
return Realm.objects.filter(country=connection.tenant, user=user, is_active=True).exists()
78+
79+
# visit lead
80+
if i.visit_lead and not has_active_realm(i.visit_lead):
81+
raise BasicValidationError(_('Visit lead is inactive or has no access'))
82+
83+
# team members
84+
inactive_members = [tm.get_full_name() for tm in i.team_members.all() if not has_active_realm(tm)]
85+
if inactive_members:
86+
raise BasicValidationError(_('Team members inactive or with no access: %s') % ', '.join(inactive_members))
87+
88+
return True

src/etools/applications/field_monitoring/planning/activity_validation/validations/state.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88

99
def tpm_partner_is_assigned_for_tpm_activity(i):
10-
if i.monitor_type == MonitoringActivity.MONITOR_TYPE_CHOICES.tpm and not i.tpm_partner:
10+
if i.monitor_type in [MonitoringActivity.MONITOR_TYPE_CHOICES.tpm, MonitoringActivity.MONITOR_TYPE_CHOICES.both] \
11+
and not i.tpm_partner:
1112
raise StateValidationError([_('Partner is not defined for TPM activity')])
1213
return True
1314

0 commit comments

Comments
 (0)