From 458494d5443e36b5159ed9b7e0ebdae570b335e0 Mon Sep 17 00:00:00 2001 From: Ema Ciupe Date: Fri, 22 Sep 2023 18:06:32 +0300 Subject: [PATCH 1/8] Decorate all "unicef_attachments.urls" with login_required --- src/etools/applications/core/urlresolvers.py | 74 +++++++++++++++++++- src/etools/config/urls.py | 4 +- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/core/urlresolvers.py b/src/etools/applications/core/urlresolvers.py index bd09b2708..293a8875f 100644 --- a/src/etools/applications/core/urlresolvers.py +++ b/src/etools/applications/core/urlresolvers.py @@ -1,6 +1,9 @@ +from functools import cached_property +from importlib import import_module + from django.conf import settings from django.db import connection -from django.urls import reverse +from django.urls import include, reverse, URLPattern, URLResolver from django_tenants.utils import get_tenant_model @@ -29,3 +32,72 @@ def build_frontend_url(*parts, user=None): frontend_url = update_url_with_kwargs(frontend_url, next=change_country_view) return frontend_url + + +class DecoratedPatterns(object): + """ + A wrapper for an urlconf that applies a decorator to all its views. + Taken from https://github.com/twidi/django-decorator-include + """ + def __init__(self, urlconf_module, decorators): + self.urlconf = urlconf_module + try: + iter(decorators) + except TypeError: + decorators = [decorators] + self.decorators = decorators + + def decorate_pattern(self, pattern): + if isinstance(pattern, URLResolver): + decorated = URLResolver( + pattern.pattern, + DecoratedPatterns(pattern.urlconf_module, self.decorators), + pattern.default_kwargs, + pattern.app_name, + pattern.namespace, + ) + else: + callback = pattern.callback + for decorator in reversed(self.decorators): + callback = decorator(callback) + decorated = URLPattern( + pattern.pattern, + callback, + pattern.default_args, + pattern.name, + ) + return decorated + + @cached_property + def urlpatterns(self): + # urlconf_module might be a valid set of patterns, so we default to it. + patterns = getattr(self.urlconf_module, 'urlpatterns', self.urlconf_module) + return [self.decorate_pattern(pattern) for pattern in patterns] + + @cached_property + def urlconf_module(self): + if isinstance(self.urlconf, str): + return import_module(self.urlconf) + else: + return self.urlconf + + @cached_property + def app_name(self): + return getattr(self.urlconf_module, 'app_name', None) + + +def decorator_include(decorators, arg, namespace=None): + """ + Works like ``django.conf.urls.include`` but takes a view decorator + or an iterable of view decorators as the first argument and applies them, + in reverse order, to all views in the included urlconf. + """ + if isinstance(arg, tuple) and len(arg) == 3 and not isinstance(arg[0], str): + # Special case where the function is used for something like `admin.site.urls`, which + # returns a tuple with the object containing the urls, the app name, and the namespace + # `include` does not support this pattern (you pass directly `admin.site.urls`, without + # using `include`) but we have to + urlconf_module, app_name, namespace = arg + else: + urlconf_module, app_name, namespace = include(arg, namespace=namespace) + return DecoratedPatterns(urlconf_module, decorators), app_name, namespace diff --git a/src/etools/config/urls.py b/src/etools/config/urls.py index d384fc199..0a2dea456 100644 --- a/src/etools/config/urls.py +++ b/src/etools/config/urls.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib import admin +from django.contrib.auth.decorators import login_required from django.urls import include, re_path from django.views.generic import TemplateView @@ -7,6 +8,7 @@ from rest_framework_swagger.renderers import OpenAPIRenderer from etools.applications.core.schemas import get_schema_view, get_swagger_view +from etools.applications.core.urlresolvers import decorator_include from etools.applications.core.views import IssueJWTRedirectView, logout_view, MainView, SocialLogoutView from etools.applications.locations.views import ( CartoDBTablesView, @@ -102,7 +104,7 @@ re_path(r'^api/v2/funds/', include('etools.applications.funds.urls')), re_path(r'^api/v2/activity/', include('unicef_snapshot.urls')), re_path(r'^api/v2/environment/', include('etools.applications.environment.urls_v2')), - re_path(r'^api/v2/attachments/', include('unicef_attachments.urls')), + re_path(r'^api/v2/attachments/', decorator_include(login_required, include('unicef_attachments.urls'))), # *************** API version 3 ****************** re_path(r'^api/v3/users/', include('etools.applications.users.urls_v3', namespace='users_v3')), From ce42970c02d04b8b488ac7200e0f1d0f0c28d043 Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 11:54:36 -0400 Subject: [PATCH 2/8] country --- src/etools/applications/field_monitoring/planning/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/etools/applications/field_monitoring/planning/models.py b/src/etools/applications/field_monitoring/planning/models.py index e6e9d8255..1d33048e8 100644 --- a/src/etools/applications/field_monitoring/planning/models.py +++ b/src/etools/applications/field_monitoring/planning/models.py @@ -338,6 +338,8 @@ def destination_str(self): @cached_property def country_pmes(self): return get_user_model().objects.filter( + profile__country=connection.tenant + ).filter( realms__group__name=PME.name, realms__country=connection.tenant, realms__is_active=True From e24d0c09d26ebb51a046dcd6b168f901ece2da44 Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 12:12:25 -0400 Subject: [PATCH 3/8] fix tests --- src/etools/applications/attachments/tests/test_views.py | 4 +++- src/etools/applications/core/tests/cases.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/attachments/tests/test_views.py b/src/etools/applications/attachments/tests/test_views.py index edbbd29eb..f51c1c427 100644 --- a/src/etools/applications/attachments/tests/test_views.py +++ b/src/etools/applications/attachments/tests/test_views.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import AnonymousUser from django.urls import resolve, reverse from rest_framework import status @@ -125,8 +126,9 @@ def test_unauthenticated_user_forbidden(self): factory = APIRequestFactory() view_info = resolve(self.url) request = factory.get(self.url) + request.user = AnonymousUser() response = view_info.func(request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_302_FOUND) def test_non_schema_user(self): user = UserFactory(profile=None, realms__data=[]) diff --git a/src/etools/applications/core/tests/cases.py b/src/etools/applications/core/tests/cases.py index e4782bc44..3b605c4b8 100644 --- a/src/etools/applications/core/tests/cases.py +++ b/src/etools/applications/core/tests/cases.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ObjectDoesNotExist from django.core.management import call_command from django.db import connection @@ -122,6 +123,7 @@ def forced_auth_req(self, method, url, user=None, data=None, request_format='jso request.tenant = user.profile.country user = user or self.user + request.user = user if user else AnonymousUser() force_authenticate(request, user=user) if "view" in kwargs: From e45f8bb5257440d38001e8164c2bd49d20d25dba Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 12:16:43 -0400 Subject: [PATCH 4/8] Update admin.py --- src/etools/applications/users/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index 98de4b781..d1047333a 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -344,7 +344,7 @@ class MultipleRealmForm(forms.ModelForm): group = forms.ModelMultipleChoiceField( widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("group").remote_field, admin.site), queryset=Group.objects.all()) organization = forms.ModelChoiceField( - widget=Select(), queryset=Organization.objects.all()) + widget=widgets.ForeignKeyRawIdWidget(Realm._meta.get_field("organization").remote_field, admin.site), queryset=Organization.objects.all()) class Meta: model = Realm From d833bacc8dc42b49838e01a645d1e081086f32a6 Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 12:17:23 -0400 Subject: [PATCH 5/8] flake --- src/etools/applications/users/admin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index d1047333a..1f4d3218d 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -338,13 +338,17 @@ def update_hact(self, request, pk): class MultipleRealmForm(forms.ModelForm): user = forms.ModelMultipleChoiceField( - widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("user").remote_field, admin.site), queryset=get_user_model().objects.all()) + widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("user").remote_field, admin.site), + queryset=get_user_model().objects.all()) country = forms.ModelMultipleChoiceField( - widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("country").remote_field, admin.site), queryset=Country.objects.all()) + widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("country").remote_field, admin.site), + queryset=Country.objects.all()) group = forms.ModelMultipleChoiceField( - widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("group").remote_field, admin.site), queryset=Group.objects.all()) + widget=widgets.ManyToManyRawIdWidget(Realm._meta.get_field("group").remote_field, admin.site), + queryset=Group.objects.all()) organization = forms.ModelChoiceField( - widget=widgets.ForeignKeyRawIdWidget(Realm._meta.get_field("organization").remote_field, admin.site), queryset=Organization.objects.all()) + widget=widgets.ForeignKeyRawIdWidget(Realm._meta.get_field("organization").remote_field, admin.site), + queryset=Organization.objects.all()) class Meta: model = Realm From 8b184a40d5f55e99e43b784ad1ee4aa5d3843890 Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 12:21:13 -0400 Subject: [PATCH 6/8] Update __init__.py --- src/etools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/__init__.py b/src/etools/__init__.py index c6daccd3b..c7acf4a94 100644 --- a/src/etools/__init__.py +++ b/src/etools/__init__.py @@ -1,2 +1,2 @@ -VERSION = __version__ = '10.1.13' +VERSION = __version__ = '11.0.1' NAME = 'eTools' From 8d96d50eb1e658285abc9a97614c081f7594e629 Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 12:36:41 -0400 Subject: [PATCH 7/8] flake fix --- src/etools/applications/users/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index 1f4d3218d..ddec7af37 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -7,7 +7,6 @@ from django.contrib.auth.forms import UserChangeForm from django.contrib.auth.models import Group from django.db import connection, router, transaction -from django.forms import Select from django.http.response import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse From da82a95b4415b357b23e5c9916f1bd4316fd40df Mon Sep 17 00:00:00 2001 From: Robert Avram Date: Fri, 22 Sep 2023 18:06:22 -0400 Subject: [PATCH 8/8] fix admin search --- src/etools/applications/partners/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index 6684705cd..482c26299 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -297,7 +297,7 @@ class InterventionAdmin( search_fields = ( 'number', 'title', - 'agreement__partner__name' + 'agreement__partner__organization__name' ) readonly_fields = ( 'total_budget',