Skip to content

Commit

Permalink
Add a voting domain to organizations
Browse files Browse the repository at this point in the history
- add a voting_domain property to organizations
- connect it to a candidate's domain
- add a feature flag for enabling the voting_domain
  • Loading branch information
tudoramariei committed Oct 14, 2024
1 parent bce807e commit ae8d572
Show file tree
Hide file tree
Showing 8 changed files with 462 additions and 214 deletions.
24 changes: 24 additions & 0 deletions backend/hub/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"email",
"phone",
"description",
"voting_domain",
"legal_representative_name",
"legal_representative_email",
"legal_representative_phone",
Expand Down Expand Up @@ -143,6 +144,12 @@ def __init__(self, *args, **kwargs):
self._set_fields_permissions()

def _set_fields_permissions(self):
if self.instance:
if FeatureFlag.flag_enabled(FLAG_CHOICES.enable_voting_domain):
self.fields["voting_domain"].disabled = self.instance.voting_domain is not None
else:
del self.fields["voting_domain"]

# All the required fields for a fully editable organization should be required in votong
if self.instance.is_fully_editable:
for field_name in self.fields:
Expand All @@ -156,6 +163,9 @@ def _set_fields_permissions(self):
for field_name in self.fields:
self.fields[field_name].disabled = True

if "voting_domain" in self.fields:
self.fields["voting_domain"].disabled = self.instance.voting_domain is not None

return

# Disable the fields that should be received from NGO Hub
Expand All @@ -175,6 +185,20 @@ def clean_email(self):
raise ValidationError(_("An organization with the same email address is already registered."))
return self.cleaned_data.get("email")

def clean_voting_domain(self):
if not FeatureFlag.flag_enabled(FLAG_CHOICES.enable_voting_domain):
return None

new_voting_domain = self.cleaned_data.get("voting_domain")
if (
self.instance
and self.instance.voting_domain is not None
and new_voting_domain != self.instance.voting_domain
):
raise ValidationError(_("Voting domain cannot be changed. Please contact the site administrator."))

return new_voting_domain

def save(self, commit=True):
if not FeatureFlag.flag_enabled(FLAG_CHOICES.enable_candidate_registration):
# This should not happen unless someone messes with the form code
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Generated by Django 4.2.15 on 2024-09-13 13:32

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("hub", "0065_candidate_photo"),
]

operations = [
migrations.AddField(
model_name="organization",
name="voting_domain",
field=models.ForeignKey(
blank=True,
help_text="The domain in which the organization can vote, support, and propose candidates – once set, the field can only be modified by the platform's administrators.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="organizations",
to="hub.domain",
verbose_name="Voting domain",
),
),
migrations.AlterField(
model_name="candidate",
name="domain",
field=models.ForeignKey(
blank=True,
help_text="The domain in which the candidate is running.",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="candidates",
to="hub.domain",
verbose_name="Domain",
),
),
migrations.AlterField(
model_name="featureflag",
name="flag",
field=models.CharField(
choices=[
("enable_org_registration", "Enable organization registration"),
("enable_org_approval", "Enable organization approvals"),
("enable_candidate_registration", "Enable candidate registration"),
("enable_candidate_supporting", "Enable candidate supporting"),
("enable_candidate_voting", "Enable candidate voting"),
("enable_candidate_confirmation", "Enable candidate confirmation"),
("enable_results_display", "Enable the display of results"),
("single_domain_round", "Voting round with just one domain (some restrictions will apply)"),
(
"global_support_round",
"Enable global support (the support of at least 10 organizations is required)",
),
("enable_voting_domain", "Enable the voting domain restriction for an organization"),
],
max_length=254,
unique=True,
verbose_name="Flag",
),
),
migrations.AlterField(
model_name="organization",
name="status",
field=models.CharField(
choices=[
("draft", "Draft"),
("pending", "Pending approval"),
("ngohub_accepted", "NGO Hub accepted"),
("accepted", "Accepted"),
("rejected", "Rejected"),
],
db_index=True,
default="draft",
max_length=30,
verbose_name="Status",
),
),
]
76 changes: 71 additions & 5 deletions backend/hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def select_public_storage():
("enable_results_display", _("Enable the display of results")),
("single_domain_round", _("Voting round with just one domain (some restrictions will apply)")),
("global_support_round", _("Enable global support (the support of at least 10 organizations is required)")),
("enable_voting_domain", _("Enable the voting domain restriction for an organization")),
)


Expand Down Expand Up @@ -212,6 +213,7 @@ class Organization(StatusModel, TimeStampedModel):
STATUS = Choices(
("draft", _("Draft")),
("pending", _("Pending approval")),
("ngohub_accepted", _("NGO Hub accepted")),
("accepted", _("Accepted")),
("rejected", _("Rejected")),
)
Expand All @@ -228,6 +230,18 @@ class Organization(StatusModel, TimeStampedModel):
city = models.ForeignKey("City", verbose_name=_("City"), on_delete=models.PROTECT, null=True, blank=True)
address = models.CharField(_("Address"), max_length=254, blank=True, default="")
registration_number = models.CharField(_("Registration number"), max_length=20, blank=True, default="")
voting_domain = models.ForeignKey(
Domain,
verbose_name=_("Voting domain"),
on_delete=models.PROTECT,
related_name="organizations",
null=True,
blank=True,
help_text=_(
"The domain in which the organization can vote, support, and propose candidates"
" – once set, the field can only be modified by the platform's administrators."
),
)

email = models.EmailField(_("Organization Email"), blank=True, default="")
phone = models.CharField(_("Organization Phone"), max_length=30, blank=True, default="")
Expand Down Expand Up @@ -350,7 +364,7 @@ def is_fully_editable(self):

@staticmethod
def required_fields():
return (
fields = [
"name",
"county",
"city",
Expand All @@ -364,7 +378,12 @@ def required_fields():
"board_council",
"last_balance_sheet",
"statute",
)
]

if FeatureFlag.flag_enabled(FLAG_CHOICES.enable_voting_domain):
fields.append("voting_domain")

return fields

@staticmethod
def ngohub_fields():
Expand Down Expand Up @@ -412,17 +431,49 @@ def is_complete(self):
+ list(map(lambda x: getattr(self, x), self.required_fields()))
)

def is_elector(self, domain=None) -> bool:
if self.status != self.STATUS.accepted:
return False

if not FeatureFlag.flag_enabled("enable_voting_domain"):
return True

if not domain:
return False

return self.voting_domain == domain

def get_absolute_url(self):
return reverse("ngo-detail", args=[self.pk])

def _remove_votes_supports_candidates(self):
# Remove votes for candidates that are not in the voting domain
for candidate in self.candidates.all():
if candidate.is_proposed:
candidate.delete()

# Remove support that the organization has given
CandidateSupporter.objects.filter(user__org=self).delete()

# Remove votes that the organization has given
CandidateVote.objects.filter(user__org=self).delete()

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

if self.pk and self.status == self.STATUS.accepted and FeatureFlag.flag_enabled("enable_voting_domain"):
old_voting_domain = Organization.objects.filter(pk=self.pk).values_list("voting_domain", flat=True).first()
if old_voting_domain and (not self.voting_domain or self.voting_domain.pk != old_voting_domain):
self._remove_votes_supports_candidates()

if self.city:
self.county = self.city.county
else:
self.county = ""

if self.status == self.STATUS.ngohub_accepted and self.voting_domain:
self.status = self.STATUS.accepted

if self.status == self.STATUS.accepted and not self.user:
owner = self.create_owner()
self.user = owner
Expand Down Expand Up @@ -486,7 +537,13 @@ class Candidate(StatusModel, TimeStampedModel):
),
)
domain = models.ForeignKey(
"Domain", verbose_name=_("Domain"), on_delete=models.PROTECT, related_name="candidates", null=True, blank=True
"Domain",
verbose_name=_("Domain"),
on_delete=models.PROTECT,
related_name="candidates",
null=True,
blank=True,
help_text=_("The domain in which the candidate is running."),
)

name = models.CharField(
Expand Down Expand Up @@ -593,7 +650,7 @@ def is_complete(self):
"""
Validate if the Org uploaded all the requested info to propose a Candidate
"""
return all(
if not all(
[
self.photo,
self.domain,
Expand All @@ -606,7 +663,16 @@ def is_complete(self):
self.declaration_of_interests,
self.fiscal_record,
]
)
):
return False

if not FeatureFlag.flag_enabled("enable_voting_domain"):
return True

if self.domain != self.org.voting_domain:
return False

return True

def get_absolute_url(self):
return reverse("candidate-detail", args=[self.pk])
Expand Down
2 changes: 1 addition & 1 deletion backend/hub/templates/hub/candidate/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ <h2 class="title border-b uppercase">

<hr/>

{% if CANDIDATE_REGISTRATION_ENABLED and user.orgs.first.is_complete and candidate.is_complete and not candidate.is_proposed %}
{% if can_propose_candidate %}
<div class="container has-text-centered">
<a href="#" class="button is-warning" style="width: 100%;" onclick="$('#id_is_proposed').val('True'); $('#candidate-update-form').submit(); return false;">
Propune candidatură
Expand Down
19 changes: 17 additions & 2 deletions backend/hub/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ def get_context_data(self, **kwargs):
and candidate.is_proposed
and candidate.org.status == Organization.STATUS.accepted
and user.orgs.first()
and user.orgs.first().status == Organization.STATUS.accepted
and user.orgs.first().is_elector(candidate.domain)
and user.has_perm("hub.support_candidate")
):
context["can_support_candidate"] = True
Expand Down Expand Up @@ -574,6 +574,19 @@ class CandidateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, HubUpdate
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["contact_email"] = settings.CONTACT_EMAIL
context["can_propose_candidate"] = False

user = self.request.user
user_org: Organization = user.orgs.first()
candidate: Candidate = self.object

if (
FeatureFlag.flag_enabled("enable_candidate_registration")
and user_org.is_complete
and candidate
and candidate.is_complete
):
context["can_propose_candidate"] = True

return context

Expand Down Expand Up @@ -624,8 +637,10 @@ def candidate_vote(request, pk):


@login_required
@permission_required_or_403("hub.delete_candidate")
def candidate_revoke(request, pk):
if not request.user.has_perm("hub.delete_candidate"):
raise PermissionDenied

if not FeatureFlag.flag_enabled("enable_candidate_supporting"):
raise PermissionDenied

Expand Down
8 changes: 6 additions & 2 deletions backend/hub/workers/update_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from civil_society_vote.common.cache import cache_decorator
from civil_society_vote.common.messaging import send_email
from hub.exceptions import NGOHubHTTPException
from hub.models import City, Organization
from hub.models import City, FeatureFlag, Organization
from django.utils.translation import gettext as _

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -196,7 +196,11 @@ def update_organization_process(organization_id: int, token: str = ""):
organization.board_council = ", ".join(board_council)

if organization.status in (Organization.STATUS.draft, Organization.STATUS.pending):
organization.status = Organization.STATUS.accepted if not errors else Organization.STATUS.pending
organization_accepted_status = Organization.STATUS.accepted
if FeatureFlag.flag_enabled("enable_voting_domain"):
organization_accepted_status = Organization.STATUS.ngohub_accepted

organization.status = organization_accepted_status if not errors else Organization.STATUS.pending

organization.ngohub_last_update_ended = timezone.now()

Expand Down
Loading

0 comments on commit ae8d572

Please sign in to comment.