Skip to content

Commit

Permalink
Update flow to allow the registration of other NGO users (#358)
Browse files Browse the repository at this point in the history
* Update flow to allow the registration of other NGO users

* fix useless walrus

* improve checks

* add back-end checks for candidates
  • Loading branch information
tudoramariei authored Nov 12, 2024
1 parent e81658f commit d3c8a66
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 74 deletions.
7 changes: 7 additions & 0 deletions backend/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
COMMITTEE_GROUP = "Comisie Electorala"
SUPPORT_GROUP = "Support Staff"
NGO_GROUP = "ONG"
NGO_USERS_GROUP = "ONG Users"


class User(AbstractUser, TimeStampedModel):
Expand Down Expand Up @@ -46,6 +47,12 @@ class Meta:
models.UniqueConstraint(Lower("email"), name="email_unique"),
]

def org_user_pks(self):
if not self.organization:
return []

return self.organization.users.values_list("pk", flat=True)

def get_cognito_id(self):
social = self.socialaccount_set.filter(provider="amazon_cognito").last()
if social:
Expand Down
7 changes: 6 additions & 1 deletion backend/hub/management/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.core.management.base import BaseCommand
from guardian.shortcuts import assign_perm

from accounts.models import COMMITTEE_GROUP, NGO_GROUP, STAFF_GROUP, SUPPORT_GROUP
from accounts.models import COMMITTEE_GROUP, NGO_GROUP, NGO_USERS_GROUP, STAFF_GROUP, SUPPORT_GROUP
from hub.models import City, FLAG_CHOICES, FeatureFlag


Expand Down Expand Up @@ -41,6 +41,11 @@ def _initialize_groups_permissions(self):
assign_perm("hub.vote_candidate", ngo_group)
assign_perm("hub.change_organization", ngo_group)

ngo_users_group: Group = Group.objects.get_or_create(name=NGO_USERS_GROUP)[0]
assign_perm("hub.support_candidate", ngo_users_group)
assign_perm("hub.vote_candidate", ngo_users_group)
assign_perm("hub.change_organization", ngo_users_group)

def _initialize_feature_flags(self):
self.stdout.write(self.style.NOTICE("Initializing feature flags..."))

Expand Down
13 changes: 8 additions & 5 deletions backend/hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,13 @@ def count_confirmations(self):
unique_confirmations = confirmations.values("user").distinct()
return unique_confirmations.count()

def update_users_permissions(self):
for org_user in self.org.users.all():
assign_perm("view_candidate", org_user, self)
assign_perm("change_candidate", org_user, self)
assign_perm("delete_candidate", org_user, self)
assign_perm("view_data_candidate", org_user, self)

def save(self, *args, **kwargs):
create = False if self.id else True

Expand All @@ -845,11 +852,7 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)

if create:
for org_user in self.org.users.all():
assign_perm("view_candidate", org_user, self)
assign_perm("change_candidate", org_user, self)
assign_perm("delete_candidate", org_user, self)
assign_perm("view_data_candidate", org_user, self)
self.update_users_permissions()


class CandidateVote(TimeStampedModel):
Expand Down
78 changes: 38 additions & 40 deletions backend/hub/social_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@
from django.urls import reverse
from django.utils.translation import gettext as _

from accounts.models import NGO_GROUP, STAFF_GROUP, User
from accounts.models import NGO_GROUP, NGO_USERS_GROUP, STAFF_GROUP, User
from hub.exceptions import (
ClosedRegistrationException,
DuplicateOrganizationException,
MissingOrganizationException,
NGOHubHTTPException,
)
Expand All @@ -43,7 +42,7 @@ def ngohub_api_get(path: str, token: str):
return response.json()


def update_user_org(org: Organization, token: str, *, in_auth_flow: bool = False) -> None:
def update_user_org(user, org: Organization, token: str, *, in_auth_flow: bool = False) -> None:
"""
Update an Organization by pulling data from NGO Hub.
Expand All @@ -52,7 +51,7 @@ def update_user_org(org: Organization, token: str, *, in_auth_flow: bool = False
"""

# Check if the new organization registration is still open
if org.status == Organization.STATUS.draft and not FeatureFlag.flag_enabled("enable_org_registration"):
if not org and not FeatureFlag.flag_enabled("enable_org_registration"):
if in_auth_flow:
raise ImmediateHttpResponse(redirect(reverse("error-org-registration-closed")))
else:
Expand All @@ -61,7 +60,7 @@ def update_user_org(org: Organization, token: str, *, in_auth_flow: bool = False
)

# If the current organization is not already linked to NGO Hub, check the NGO Hub API for the data
if not org.ngohub_org_id:
if not org:
ngohub_org = ngohub_api_get("organization-profile/", token)

# Check that an NGO Hub organization appears only once in VotONG
Expand All @@ -72,17 +71,19 @@ def update_user_org(org: Organization, token: str, *, in_auth_flow: bool = False
else:
raise MissingOrganizationException(_("There is no NGO Hub organization for this VotONG user."))

# Check that the current user has an NGO Hub organization
if Organization.objects.filter(ngohub_org_id=ngohub_id).exclude(pk=org.pk).count():
if in_auth_flow:
raise ImmediateHttpResponse(redirect(reverse("error-org-duplicate")))
else:
raise DuplicateOrganizationException(
_("This NGO Hub organization already exists for another VotONG user.")
)
# Check that the user's organization doesn't already exist in VotONG
if Organization.objects.filter(ngohub_org_id=ngohub_id).exists():
org = Organization.objects.get(ngohub_org_id=ngohub_id)
user.organization = org
user.save()

if org.candidate:
org.candidate.update_users_permissions()
else:
org = create_user_org(user)

org.ngohub_org_id = ngohub_id
org.save()
org.ngohub_org_id = ngohub_id
org.save()

update_organization(org.id, token)

Expand Down Expand Up @@ -118,6 +119,24 @@ def create_user_org(user: User) -> Organization:
return org


def get_user_for_org(user, user_token: str, user_role: str):
if not check_app_enabled_in_ngohub(user_token):
if user.is_active:
user.is_active = False
user.save()

raise ImmediateHttpResponse(redirect(reverse("error-app-missing")))
elif not user.is_active:
user.is_active = True
user.save()

# Add the user to the NGO group
ngo_group: Group = Group.objects.get(name=user_role)
user.groups.add(ngo_group)

return user.organization


def update_user_information(user: User, token: str):
try:
user_profile: Dict = ngohub_api_get("profile/", token)
Expand All @@ -140,32 +159,15 @@ def update_user_information(user: User, token: str):

user.groups.add(Group.objects.get(name=STAFF_GROUP))
user.groups.remove(Group.objects.get(name=NGO_GROUP))
user.groups.remove(Group.objects.get(name=NGO_USERS_GROUP))

return None

elif user_role == settings.NGOHUB_ROLE_NGO_ADMIN:
if not check_app_enabled_in_ngohub(token):
if user.is_active:
user.is_active = False
user.save()

raise ImmediateHttpResponse(redirect(reverse("error-app-missing")))
elif not user.is_active:
user.is_active = True
user.save()

# Add the user to the NGO group
ngo_group: Group = Group.objects.get(name=NGO_GROUP)
user.groups.add(ngo_group)

if not (org := user.organization):
org = None

return org
return get_user_for_org(user, token, NGO_GROUP)

elif user_role == settings.NGOHUB_ROLE_NGO_EMPLOYEE:
# Employees cannot have organizations
raise ImmediateHttpResponse(redirect(reverse("error-user-role")))
return get_user_for_org(user, token, NGO_USERS_GROUP)

else:
# Unknown user role
Expand All @@ -182,11 +184,7 @@ def common_user_init(sociallogin: SocialLogin) -> User:
if user.groups.filter(name=STAFF_GROUP).exists():
return user

# Start the import of initial data from NGO Hub
if not org:
org = create_user_org(user)

update_user_org(org, sociallogin.token.token, in_auth_flow=True)
update_user_org(user, org, sociallogin.token.token, in_auth_flow=True)

return user

Expand Down
9 changes: 1 addition & 8 deletions backend/hub/templatetags/hub_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@
from django.utils.translation import gettext as _

from accounts.models import User
from hub.models import CandidateConfirmation, Organization
from hub.models import CandidateConfirmation

register = template.Library()


@register.filter
def can_vote(user):
if Organization.objects.filter(users=user, status=Organization.STATUS.accepted).count():
return True
return False


@register.filter
def already_confirmed_candidate_status(user, candidate):
if CandidateConfirmation.objects.filter(user=user, candidate=candidate).exists():
Expand Down
48 changes: 28 additions & 20 deletions backend/hub/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from guardian.mixins import LoginRequiredMixin, PermissionRequiredMixin
from sentry_sdk import capture_message

from accounts.models import COMMITTEE_GROUP, NGO_GROUP, STAFF_GROUP, SUPPORT_GROUP, User
from accounts.models import COMMITTEE_GROUP, NGO_GROUP, NGO_USERS_GROUP, STAFF_GROUP, SUPPORT_GROUP, User
from civil_society_vote.common.messaging import send_email
from hub.forms import (
CandidateRegisterForm,
Expand Down Expand Up @@ -244,7 +244,7 @@ class ElectorCandidatesListView(LoginRequiredMixin, SearchMixin):
template_name = "hub/ngo/votes.html"

def get_queryset(self):
if not self.request.user.groups.filter(name__in=[NGO_GROUP]).exists():
if not self.request.user.groups.filter(name__in=[NGO_GROUP, NGO_USERS_GROUP]).exists():
raise PermissionDenied

voted_candidates = CandidateVote.objects.filter(user=self.request.user)
Expand Down Expand Up @@ -682,16 +682,18 @@ def _get_candidate_support_context(user: User, candidate: Candidate) -> Dict[str
if not user.has_perm("hub.support_candidate"):
return context

if not user.organization:
organization = user.organization

if not organization:
return context

# An organization can support candidates from any domain
if not user.organization.is_elector(user.organization.voting_domain):
if not organization.is_elector(organization.voting_domain):
return context

context["can_support_candidate"] = True

if CandidateSupporter.objects.filter(user=user, candidate=candidate).exists():
if CandidateSupporter.objects.filter(user__pk__in=user.org_user_pks(), candidate=candidate).exists():
context["supported_candidate"] = True

return context
Expand Down Expand Up @@ -736,16 +738,18 @@ def _get_candidate_vote_context(user: User, candidate: Candidate) -> Dict[str, b
if not user.has_perm("hub.vote_candidate"):
return context

domain = candidate.domain

# An organization can only vote for candidates from its own domain
if not user.organization.is_elector(candidate.domain):
if not user.organization.is_elector(domain):
return context

context["can_vote_candidate"] = True

if CandidateVote.objects.filter(user=user, candidate=candidate).exists():
if CandidateVote.objects.filter(user__pk__in=user.org_user_pks(), candidate=candidate).exists():
context["voted_candidate"] = True

if CandidateVote.objects.filter(user=user, domain=candidate.domain).count() >= candidate.domain.seats:
if CandidateVote.objects.filter(user__in=user.org_user_pks(), domain=domain).count() >= domain.seats:
context["used_all_domain_votes"] = True

return context
Expand Down Expand Up @@ -893,14 +897,19 @@ def candidate_vote(request, pk):
if not FeatureFlag.flag_enabled(FLAG_CHOICES.enable_candidate_voting):
raise PermissionDenied

candidate = get_object_or_404(Candidate, pk=pk, is_proposed=True, status=Candidate.STATUS.confirmed)

user: User = request.user
user_org = user.organization
if user_org.status != Organization.STATUS.accepted:
raise PermissionDenied

if CandidateVote.objects.filter(user__pk__in=request.user.org_user_pks(), candidate=candidate).exists():
raise PermissionDenied

try:
candidate = Candidate.objects.get(
pk=pk, org__status=Organization.STATUS.accepted, status=Candidate.STATUS.confirmed, is_proposed=True
)
vote = CandidateVote.objects.create(user=request.user, candidate=candidate)
except Exception:
if settings.ENABLE_SENTRY:
capture_message(f"User {request.user} tried to vote for candidate {pk} again.", level="warning")
raise PermissionDenied

if settings.VOTE_AUDIT_EMAIL:
Expand Down Expand Up @@ -947,22 +956,21 @@ def candidate_support(request, pk):
if not FeatureFlag.flag_enabled("enable_candidate_supporting"):
raise PermissionDenied

candidate = get_object_or_404(Candidate, pk=pk, is_proposed=True)

user: User = request.user
user_org = user.organization
if user_org.status != Organization.STATUS.accepted:
raise PermissionDenied

candidate = get_object_or_404(Candidate, pk=pk, is_proposed=True)

if candidate.org == user_org:
return redirect("candidate-detail", pk=pk)

try:
supporter = CandidateSupporter.objects.get(user=request.user, candidate=candidate)
except CandidateSupporter.DoesNotExist:
CandidateSupporter.objects.create(user=request.user, candidate=candidate)
else:
supporter = CandidateSupporter.objects.filter(user__pk__in=request.user.org_user_pks(), candidate=candidate)
if supporter.exists():
supporter.delete()
else:
CandidateSupporter.objects.create(user=request.user, candidate=candidate)

return redirect("candidate-detail", pk=pk)

Expand Down

0 comments on commit d3c8a66

Please sign in to comment.