From 8c6d24579d9a5418abb7c0d85e609f82063f90f2 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 10:27:24 +0200 Subject: [PATCH 01/17] remove '~' from urls --- commcare_connect/users/tests/test_urls.py | 8 ++++---- commcare_connect/users/urls.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/commcare_connect/users/tests/test_urls.py b/commcare_connect/users/tests/test_urls.py index fe736774..f2dbe4af 100644 --- a/commcare_connect/users/tests/test_urls.py +++ b/commcare_connect/users/tests/test_urls.py @@ -9,10 +9,10 @@ def test_detail(user: User): def test_update(): - assert reverse("users:update") == "/users/~update/" - assert resolve("/users/~update/").view_name == "users:update" + assert reverse("users:update") == "/users/update/" + assert resolve("/users/update/").view_name == "users:update" def test_redirect(): - assert reverse("users:redirect") == "/users/~redirect/" - assert resolve("/users/~redirect/").view_name == "users:redirect" + assert reverse("users:redirect") == "/users/redirect/" + assert resolve("/users/redirect/").view_name == "users:redirect" diff --git a/commcare_connect/users/urls.py b/commcare_connect/users/urls.py index 5b1e33cf..667cc78f 100644 --- a/commcare_connect/users/urls.py +++ b/commcare_connect/users/urls.py @@ -4,7 +4,7 @@ app_name = "users" urlpatterns = [ - path("~redirect/", view=user_redirect_view, name="redirect"), - path("~update/", view=user_update_view, name="update"), + path("redirect/", view=user_redirect_view, name="redirect"), + path("update/", view=user_update_view, name="update"), path("/", view=user_detail_view, name="detail"), ] From 43328485f5b9b55d6a32a94ec7ebe3ad71e31894 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 10:36:38 +0200 Subject: [PATCH 02/17] add utility to promote user to superuser --- commcare_connect/users/management/__init__.py | 0 .../users/management/commands/__init__.py | 0 .../commands/promote_user_to_superuser.py | 20 +++++++++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 commcare_connect/users/management/__init__.py create mode 100644 commcare_connect/users/management/commands/__init__.py create mode 100644 commcare_connect/users/management/commands/promote_user_to_superuser.py diff --git a/commcare_connect/users/management/__init__.py b/commcare_connect/users/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/users/management/commands/__init__.py b/commcare_connect/users/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/users/management/commands/promote_user_to_superuser.py b/commcare_connect/users/management/commands/promote_user_to_superuser.py new file mode 100644 index 00000000..e576fec6 --- /dev/null +++ b/commcare_connect/users/management/commands/promote_user_to_superuser.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand, CommandError + +from commcare_connect.users.models import User + + +class Command(BaseCommand): + help = 'Promotes the given user to a superuser and provides admin access.' + + def add_arguments(self, parser): + parser.add_argument('email', type=str) + + def handle(self, email, **options): + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + raise CommandError(f'No user with email {email} found!') + user.is_superuser = True + user.is_staff = True + user.save() + print(f'{email} successfully promoted to superuser and can now access the admin site') From 25c2cc2d1269c767a5af88ee24e89845ee7c69f4 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 10:38:29 +0200 Subject: [PATCH 03/17] make org membership admin inline with org admin --- commcare_connect/users/admin.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/commcare_connect/users/admin.py b/commcare_connect/users/admin.py index cedfc074..3a374069 100644 --- a/commcare_connect/users/admin.py +++ b/commcare_connect/users/admin.py @@ -44,14 +44,16 @@ class UserAdmin(auth_admin.UserAdmin): ) +class UserOrganizationMembershipInline(admin.TabularInline): + list_display = ["organization", "user", "role"] + model = UserOrganizationMembership + + @admin.register(Organization) class OrganizationAdmin(admin.ModelAdmin): form = OrganizationCreationForm list_display = ["name", "created_by"] search_fields = ["name"] ordering = ["name"] + inlines = [UserOrganizationMembershipInline] - -@admin.register(UserOrganizationMembership) -class UserOrganizationMembershipAdmin(admin.ModelAdmin): - list_display = ["organization", "user", "role"] From ac32714f55f910c3d864c6ce696e3e2955876fb5 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 11:19:02 +0200 Subject: [PATCH 04/17] make nav links active --- commcare_connect/templates/base.html | 17 +++++---- commcare_connect/users/views.py | 3 ++ commcare_connect/web/__init__.py | 0 commcare_connect/web/apps.py | 6 +++ commcare_connect/web/templatetags/__init__.py | 0 .../web/templatetags/active_link.py | 38 +++++++++++++++++++ config/settings/base.py | 1 + 7 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 commcare_connect/web/__init__.py create mode 100644 commcare_connect/web/apps.py create mode 100644 commcare_connect/web/templatetags/__init__.py create mode 100644 commcare_connect/web/templatetags/active_link.py diff --git a/commcare_connect/templates/base.html b/commcare_connect/templates/base.html index c059bc91..86eb92a3 100644 --- a/commcare_connect/templates/base.html +++ b/commcare_connect/templates/base.html @@ -1,4 +1,5 @@ -{% load static i18n %} +{% load static i18n %} +{% load active_link %} {% get_current_language as LANGUAGE_CODE %} @@ -38,30 +39,30 @@ {% endblock content %} From da5454e74f912dda3e7e587b59e6a57c1c1a8a29 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 11:50:55 +0200 Subject: [PATCH 13/17] run lint --- commcare_connect/users/admin.py | 1 - commcare_connect/users/helpers.py | 2 +- .../management/commands/promote_user_to_superuser.py | 8 ++++---- commcare_connect/users/middleware.py | 4 ++-- commcare_connect/web/templatetags/active_link.py | 8 ++++---- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/commcare_connect/users/admin.py b/commcare_connect/users/admin.py index 3a374069..5f77f6f7 100644 --- a/commcare_connect/users/admin.py +++ b/commcare_connect/users/admin.py @@ -56,4 +56,3 @@ class OrganizationAdmin(admin.ModelAdmin): search_fields = ["name"] ordering = ["name"] inlines = [UserOrganizationMembershipInline] - diff --git a/commcare_connect/users/helpers.py b/commcare_connect/users/helpers.py index e577001c..61a0be37 100644 --- a/commcare_connect/users/helpers.py +++ b/commcare_connect/users/helpers.py @@ -5,7 +5,7 @@ def get_organization_for_request(request, view_kwargs): if not request.user.is_authenticated: return - org_slug = view_kwargs.get('org_slug', None) + org_slug = view_kwargs.get("org_slug", None) if org_slug: try: return Organization.objects.get(slug=org_slug, memberships__user=request.user) diff --git a/commcare_connect/users/management/commands/promote_user_to_superuser.py b/commcare_connect/users/management/commands/promote_user_to_superuser.py index e576fec6..6a55de36 100644 --- a/commcare_connect/users/management/commands/promote_user_to_superuser.py +++ b/commcare_connect/users/management/commands/promote_user_to_superuser.py @@ -4,17 +4,17 @@ class Command(BaseCommand): - help = 'Promotes the given user to a superuser and provides admin access.' + help = "Promotes the given user to a superuser and provides admin access." def add_arguments(self, parser): - parser.add_argument('email', type=str) + parser.add_argument("email", type=str) def handle(self, email, **options): try: user = User.objects.get(email=email) except User.DoesNotExist: - raise CommandError(f'No user with email {email} found!') + raise CommandError(f"No user with email {email} found!") user.is_superuser = True user.is_staff = True user.save() - print(f'{email} successfully promoted to superuser and can now access the admin site') + print(f"{email} successfully promoted to superuser and can now access the admin site") diff --git a/commcare_connect/users/middleware.py b/commcare_connect/users/middleware.py index 638def4c..08cd35d6 100644 --- a/commcare_connect/users/middleware.py +++ b/commcare_connect/users/middleware.py @@ -6,14 +6,14 @@ def _get_organization(request, view_kwargs): - if not hasattr(request, '_cached_org'): + if not hasattr(request, "_cached_org"): team = get_organization_for_request(request, view_kwargs) request._cached_org = team return request._cached_org def _get_org_membership(request): - if not hasattr(request, '_cached_org_membership'): + if not hasattr(request, "_cached_org_membership"): org = request.org membership = None if org: diff --git a/commcare_connect/web/templatetags/active_link.py b/commcare_connect/web/templatetags/active_link.py index 6148ccbd..2509cfb6 100644 --- a/commcare_connect/web/templatetags/active_link.py +++ b/commcare_connect/web/templatetags/active_link.py @@ -4,7 +4,7 @@ @register.simple_tag(takes_context=True) -def active_link(context, viewnames, css_class='active', inactive_class='', namespace=None, *args, **kwargs): +def active_link(context, viewnames, css_class="active", inactive_class="", namespace=None, *args, **kwargs): """ Renders the given CSS class if the request path matches the path of the view. :param context: The context where the tag was called. Used to access the request object. @@ -14,14 +14,14 @@ def active_link(context, viewnames, css_class='active', inactive_class='', names :param namespace: The namespace of the view or views. This can also be provided in the view name. :return: """ - request = context.get('request') + request = context.get("request") if request is None: # Can't work without the request object. - return '' + return "" current_url_name = request.resolver_match.url_name namespaces = request.resolver_match.namespaces active = False - views = viewnames.split('||') + views = viewnames.split("||") for view_name in views: view_namespace = namespace if ":" in view_name: From 30a70db5b3b7c544428f47cfc2aaab2105b5e57d Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 11:51:36 +0200 Subject: [PATCH 14/17] update readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index b00235eb..7d9e9803 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ need to edit some settings. # install requirements $ pip install -r requirements-dev.txt + # install git hooks + $ pre-commit install + $ pre-commit run -a + # create env file and edit the settings as needed (or export settings directly) $ cp .env_template .env @@ -46,6 +50,10 @@ Some useful command are available via the `tasks.py` file: $ python manage.py createsuperuser +- To promote a user to superuser, use this command: + + $ python manage.py promote_user_to_superuser + For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users. ### Test coverage From bb9c9b4f9b8c355e47722c08dc20b5129eb8a02a Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 12:22:52 +0200 Subject: [PATCH 15/17] update test --- commcare_connect/users/tests/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/commcare_connect/users/tests/test_views.py b/commcare_connect/users/tests/test_views.py index 253678ff..2a7ed4ed 100644 --- a/commcare_connect/users/tests/test_views.py +++ b/commcare_connect/users/tests/test_views.py @@ -71,6 +71,7 @@ def test_get_redirect_url(self, user: User, rf: RequestFactory): view = UserRedirectView() request = rf.get("/fake-url") request.user = user + request.org = None view.request = request assert view.get_redirect_url() == f"/users/{user.pk}/" From 8946905a336936e6095414849b6cf38e83237613 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 12:23:13 +0200 Subject: [PATCH 16/17] test login redirect for org user --- commcare_connect/conftest.py | 19 ++++++++++++++-- commcare_connect/users/tests/factories.py | 25 +++++++++++++++++++++- commcare_connect/users/tests/test_views.py | 13 ++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/commcare_connect/conftest.py b/commcare_connect/conftest.py index 0530d6d2..38a28179 100644 --- a/commcare_connect/conftest.py +++ b/commcare_connect/conftest.py @@ -1,7 +1,7 @@ import pytest -from commcare_connect.users.models import User -from commcare_connect.users.tests.factories import UserFactory +from commcare_connect.users.models import Organization, User +from commcare_connect.users.tests.factories import OrgWithUsersFactory, UserFactory @pytest.fixture(autouse=True) @@ -9,6 +9,21 @@ def media_storage(settings, tmpdir): settings.MEDIA_ROOT = tmpdir.strpath +@pytest.fixture +def organization(db) -> Organization: + return OrgWithUsersFactory() + + @pytest.fixture def user(db) -> User: return UserFactory() + + +@pytest.fixture +def org_user_member(organization) -> User: + return organization.memberships.filter(role="member").first().user + + +@pytest.fixture +def org_user_admin(organization) -> User: + return organization.memberships.filter(role="admin").first().user diff --git a/commcare_connect/users/tests/factories.py b/commcare_connect/users/tests/factories.py index caa14cf4..4fe3d34c 100644 --- a/commcare_connect/users/tests/factories.py +++ b/commcare_connect/users/tests/factories.py @@ -2,9 +2,11 @@ from typing import Any from django.contrib.auth import get_user_model -from factory import Faker, post_generation +from factory import Faker, RelatedFactory, SubFactory, post_generation from factory.django import DjangoModelFactory +from commcare_connect.users.models import UserOrganizationMembership + class UserFactory(DjangoModelFactory): email = Faker("email") @@ -29,3 +31,24 @@ def password(self, create: bool, extracted: Sequence[Any], **kwargs): class Meta: model = get_user_model() django_get_or_create = ["email"] + + +class OrganizationFactory(DjangoModelFactory): + name = Faker("company") + + class Meta: + model = "users.Organization" + + +class MembershipFactory(DjangoModelFactory): + class Meta: + model = UserOrganizationMembership + + user = SubFactory(UserFactory) + organization = SubFactory(OrganizationFactory) + role = "admin" + + +class OrgWithUsersFactory(OrganizationFactory): + admin = RelatedFactory(MembershipFactory, "organization", role="admin") + member = RelatedFactory(MembershipFactory, "organization", role="member") diff --git a/commcare_connect/users/tests/test_views.py b/commcare_connect/users/tests/test_views.py index 2a7ed4ed..7032a056 100644 --- a/commcare_connect/users/tests/test_views.py +++ b/commcare_connect/users/tests/test_views.py @@ -9,7 +9,7 @@ from django.urls import reverse from commcare_connect.users.forms import UserAdminChangeForm -from commcare_connect.users.models import User +from commcare_connect.users.models import Organization, User from commcare_connect.users.tests.factories import UserFactory from commcare_connect.users.views import UserRedirectView, UserUpdateView, user_detail_view @@ -76,6 +76,17 @@ def test_get_redirect_url(self, user: User, rf: RequestFactory): view.request = request assert view.get_redirect_url() == f"/users/{user.pk}/" + def test_get_redirect_url_for_org_user( + self, organization: Organization, org_user_member: User, rf: RequestFactory + ): + view = UserRedirectView() + request = rf.get("/fake-url") + request.user = org_user_member + request.org = organization + + view.request = request + assert view.get_redirect_url() == f"/a/{organization.slug}/opportunity/" + class TestUserDetailView: def test_authenticated(self, user: User, rf: RequestFactory): From 326020e57c4d2c62e9b50e418c5636430126e778 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Tue, 25 Jul 2023 12:23:26 +0200 Subject: [PATCH 17/17] fix user save bug --- commcare_connect/users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commcare_connect/users/models.py b/commcare_connect/users/models.py index 69598244..1726381d 100644 --- a/commcare_connect/users/models.py +++ b/commcare_connect/users/models.py @@ -43,7 +43,7 @@ class Organization(BaseModel): def save(self, *args, **kwargs): if not self.id: self.slug = slugify_uniquely(self.name, self.__class__) - super().save(*args, *kwargs) + super().save(*args, **kwargs) def __str__(self): return self.slug