Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send an email link to a comission member for confirmations deletion #365

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions backend/accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@
from django.contrib.admin.sites import NotRegistered
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
from django.contrib.auth.models import Group as BaseGroup
from django.urls import reverse_lazy
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import QuerySet
from django.http import HttpRequest
from django.urls import reverse_lazy, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from impersonate.admin import UserAdminImpersonateMixin

from civil_society_vote.common.admin import BasePermissionsAdmin
from civil_society_vote.common.messaging import send_email
from hub.utils import create_expiring_url_token

from .models import (
CommissionUser,
COMMITTEE_GROUP_READ_ONLY,
COMMITTEE_GROUP,
GroupProxy,
STAFF_GROUP,
SUPPORT_GROUP,
User,
)

from .models import GroupProxy, User

# Remove the default admins for User and Group
try:
Expand Down Expand Up @@ -45,6 +59,8 @@ class UserAdmin(UserAdminImpersonateMixin, BasePermissionsAdmin):

search_fields = ("email", "first_name", "last_name")

actions = ("request_confirmations_deletion",)

readonly_fields = (
"email",
"password",
Expand Down Expand Up @@ -101,6 +117,29 @@ class UserAdmin(UserAdminImpersonateMixin, BasePermissionsAdmin):
),
)

@admin.action(description=_("Request confirmations deletion"))
def request_confirmations_deletion(self, request: HttpRequest, queryset: QuerySet[User]):

current_site = get_current_site(request)
protocol = "https" if request.is_secure() else "http"

for user in queryset:
if not user.in_commission_groups():
continue

deletion_link_path = reverse("reset-candidate-confirmations", args=(create_expiring_url_token(user.pk),))
deletion_link = f"{protocol}://{current_site.domain}{deletion_link_path}"

send_email(
subject="[VOTONG] Resetare confirmari candidati",
context={
"deletion_link": deletion_link,
},
to_emails=[user.email],
text_template="hub/emails/08_delete_confirmations.txt",
html_template="hub/emails/08_delete_confirmations.html",
)

def change_view(self, request, object_id, form_url="", extra_context=None):
extra_context = extra_context or {}
extra_context["user_id"] = object_id
Expand Down Expand Up @@ -133,5 +172,23 @@ def get_groups(self, obj: User):
get_groups.short_description = _("Groups")


@admin.register(CommissionUser)
class ComissionAdmin(UserAdmin):
"""
User admin which only handles electoral commission users
"""

# TODO: Use only relevant filters
# TODO: Move request_confirmations_deletion to be available only to this admin

def get_queryset(self, request) -> QuerySet:
return (
super()
.get_queryset(request)
.filter(groups__name__in=[COMMITTEE_GROUP, COMMITTEE_GROUP_READ_ONLY])
.exclude(groups__name__in=[STAFF_GROUP, SUPPORT_GROUP])
)


@admin.register(GroupProxy)
class GroupAdmin(BaseGroupAdmin, BasePermissionsAdmin): ...
29 changes: 29 additions & 0 deletions backend/accounts/migrations/0012_commissionuser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.16 on 2024-11-23 13:58

import django.contrib.auth.models
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("accounts", "0011_groupproxy_user_created_user_modified_and_more"),
]

operations = [
migrations.CreateModel(
name="CommissionUser",
fields=[],
options={
"verbose_name": "Utilizator Comisie Electorală",
"verbose_name_plural": "Utilizatori Comisie Electorală",
"proxy": True,
"indexes": [],
"constraints": [],
},
bases=("accounts.user",),
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]
7 changes: 7 additions & 0 deletions backend/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,12 @@ class Meta:
verbose_name_plural = _("Groups")


class CommissionUser(User):
class Meta:
proxy = True
verbose_name = _("Commission user")
verbose_name_plural = _("Commission users")


auditlog.register(User, exclude_fields=["password"])
auditlog.register(GroupProxy)
5 changes: 5 additions & 0 deletions backend/civil_society_vote/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
https://docs.djangoproject.com/en/2.2/ref/settings/
"""

import hashlib
import os
from copy import deepcopy
from pathlib import Path
Expand Down Expand Up @@ -91,6 +92,7 @@
ENABLE_ORG_REGISTRATION_FORM=(bool, False),
CURRENT_EDITION_YEAR=(int, 2024),
CURRENT_EDITION_TYPE=(str, "ces"),
EXPIRING_URL_DELTA=(int, 72 * 60 * 60), # 72 hours
# email settings
EMAIL_SEND_METHOD=(str, "async"),
EMAIL_BACKEND=(str, "django.core.mail.backends.console.EmailBackend"),
Expand Down Expand Up @@ -121,6 +123,7 @@

# SECURITY WARNING: keep the secret key used in production secret
SECRET_KEY = env("SECRET_KEY")
SECRET_KEY_HASH = hashlib.sha256(f"{SECRET_KEY}".encode()).hexdigest()

DEBUG = env("DEBUG")
ENVIRONMENT = env.str("ENVIRONMENT")
Expand Down Expand Up @@ -645,3 +648,5 @@ def show_toolbar(request):

# How many previous year reports to require for a candidate proposal
PREV_REPORTS_REQUIRED_FOR_PROPOSAL = 3

EXPIRING_URL_DELTA = env.int("EXPIRING_URL_DELTA")
12 changes: 6 additions & 6 deletions backend/hub/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.contrib.auth.models import Group
from django.contrib.sites.shortcuts import get_current_site
from django.core.handlers.wsgi import WSGIRequest
from django.db.models import Count, QuerySet
from django.http import HttpRequest
from django.shortcuts import redirect, render
from django.urls import path, reverse
from django.utils.http import urlencode
Expand Down Expand Up @@ -210,15 +210,15 @@ def send_confirm_email_to_committee(request, candidate, to_email):


def _set_candidates_status(
request: WSGIRequest,
request: HttpRequest,
queryset: QuerySet[Candidate],
status: str,
send_committee_confirmation: bool = True,
):
committee_emails = Group.objects.get(name=COMMITTEE_GROUP).user_set.all().values_list("email", flat=True)

for candidate in queryset:
# only take action if there is a chance in the status
# only take action if there is a change in the status
if candidate.status != status:
CandidateConfirmation.objects.filter(candidate=candidate).delete()

Expand All @@ -231,21 +231,21 @@ def _set_candidates_status(
queryset.update(status=status)


def reject_candidates(_, request: WSGIRequest, queryset: QuerySet[Candidate]):
def reject_candidates(_, request: HttpRequest, queryset: QuerySet[Candidate]):
_set_candidates_status(request, queryset, Candidate.STATUS.rejected)


reject_candidates.short_description = _("Set selected candidates status to REJECTED")


def accept_candidates(_, request: WSGIRequest, queryset: QuerySet[Candidate]):
def accept_candidates(_, request: HttpRequest, queryset: QuerySet[Candidate]):
_set_candidates_status(request, queryset, Candidate.STATUS.accepted)


accept_candidates.short_description = _("Set selected candidates status to ACCEPTED")


def pending_candidates(_, request: WSGIRequest, queryset: QuerySet[Candidate]):
def pending_candidates(_, request: HttpRequest, queryset: QuerySet[Candidate]):
_set_candidates_status(request, queryset, Candidate.STATUS.pending, send_committee_confirmation=False)


Expand Down
4 changes: 2 additions & 2 deletions backend/hub/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Any, Dict

from django.conf import settings
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpRequest
from django.urls import reverse

from hub.models import FLAG_CHOICES, FeatureFlag, SETTINGS_CHOICES


def hub_settings(_: WSGIRequest) -> Dict[str, Any]:
def hub_settings(_: HttpRequest) -> Dict[str, Any]:
flags = {k: v for k, v in FeatureFlag.objects.all().values_list("flag", "is_enabled")}

register_url = settings.NGOHUB_APP_BASE
Expand Down
1 change: 1 addition & 0 deletions backend/hub/management/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def _initialize_groups_permissions(self):

committee_group: Group = Group.objects.get_or_create(name=COMMITTEE_GROUP)[0]
assign_perm("hub.approve_candidate", committee_group)
assign_perm("hub.reset_approve_candidate", committee_group)
assign_perm("hub.view_data_candidate", committee_group)

committee_group_read_only: Group = Group.objects.get_or_create(name=COMMITTEE_GROUP_READ_ONLY)[0]
Expand Down
28 changes: 28 additions & 0 deletions backend/hub/migrations/0076_alter_candidate_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 4.2.16 on 2024-11-23 13:58

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("hub", "0075_alter_candidate_statement"),
]

operations = [
migrations.AlterModelOptions(
name="candidate",
options={
"ordering": ["name"],
"permissions": (
("view_data_candidate", "View data candidate"),
("approve_candidate", "Approve candidate"),
("reset_approve_candidate", "Reset candidate approval"),
("support_candidate", "Support candidate"),
("vote_candidate", "Vote candidate"),
),
"verbose_name": "Candidate",
"verbose_name_plural": "Candidates",
},
),
]
1 change: 1 addition & 0 deletions backend/hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,7 @@ class Meta:
permissions = (
("view_data_candidate", "View data candidate"),
("approve_candidate", "Approve candidate"),
("reset_approve_candidate", "Reset candidate approval"),
("support_candidate", "Support candidate"),
("vote_candidate", "Vote candidate"),
)
Expand Down
18 changes: 17 additions & 1 deletion backend/hub/templates/hub/committee/candidates.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,25 @@
<div class="container">
<h2 class="title border-b uppercase">
{% if filtering == "pending" %}

{% trans 'Pending candidates' %} ({{ counters.candidates_pending }})
{% else %}

{% elif filtering == "confirmed" %}

{% trans 'Validated candidates' %} ({{ counters.candidates_confirmed }})

{% elif filtering == "rejected" %}

{% trans 'Rejected candidates' %} ({{ counters.candidates_rejected }})

{% elif filtering == "accepted" %}

{% trans 'Accepted candidates' %} ({{ counters.candidates_accepted }})

{% else %}

&nbsp;

{% endif %}
</h2>

Expand Down
19 changes: 19 additions & 0 deletions backend/hub/templates/hub/committee/delete_confirmations.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends 'hub/ngo/base.html' %}
{% load static i18n %}
{% load spurl %}
{% load hub_tags %}


{% block content %}

<div class="container">
<form action="" method="post">
{% csrf_token %}
<div class="section-title">{% trans "Reset candidate confirmations" %}</div>
<p class="section-text">{% trans "Resetting the candidate confirmations is irreversible. Are you sure you want to continue?" %}</p>
<p><input type="submit" value="{% trans 'Continue' %}" class="button is-warning has-text-weight-bold"></p>
</div>
</form>
</div>

{% endblock %}
10 changes: 10 additions & 0 deletions backend/hub/templates/hub/emails/08_delete_confirmations.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% extends "emails/base_email.html" %}

{% block content %}
<h3>Administratorul site-ului votong.ro a marcat pentru ștergere confirmările date de tine candidaților.</h3>

<p>
Urmează acest link pentru a aproba acțiunea:
<a href="{{ deletion_link|safe }}">{{ deletion_link|safe }}</a>
</p>
{% endblock %}
3 changes: 3 additions & 0 deletions backend/hub/templates/hub/emails/08_delete_confirmations.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Administratorul site-ului votong.ro a marcat pentru ștergere confirmările date de tine candidaților.

Urmează acest link pentru a aproba acțiunea: {{ deletion_link|safe }}
6 changes: 6 additions & 0 deletions backend/hub/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
candidate_vote,
organization_vote,
update_organization_information,
reset_candidate_confirmations,
)

urlpatterns = [
Expand All @@ -48,6 +49,11 @@
path(
_("candidates/ces-results/"), RedirectView.as_view(pattern_name="results", permanent=True), name="ces-results"
),
path(
_("committee/reset-confirmations/<slug:url_token>/"),
reset_candidate_confirmations,
name="reset-candidate-confirmations",
),
path(_("committee/ngos/"), CommitteeOrganizationListView.as_view(), name="committee-ngos"),
path(_("committee/candidates/"), CommitteeCandidatesListView.as_view(), name="committee-candidates"),
path(_("ngos/"), OrganizationListView.as_view(), name="ngos"),
Expand Down
Loading