Skip to content

Commit

Permalink
Merge pull request #201 from WorldconVotingSystems/permission-removal…
Browse files Browse the repository at this point in the history
…-reverse

Permission removal uno reverse card
  • Loading branch information
offbyone authored Jan 21, 2025
2 parents b4d6ddf + 0962186 commit 235e702
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 17 deletions.
15 changes: 14 additions & 1 deletion src/nomnom/nominate/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import UTC, datetime
from collections.abc import Iterable
from datetime import UTC, datetime

from django.apps import apps
from django.conf import settings
Expand Down Expand Up @@ -297,6 +297,15 @@ def field_names(self) -> list[str]:
][: self.fields]


class NominationValidManager(models.Manager):
def get_queryset(self) -> models.QuerySet:
return (
super()
.get_queryset()
.filter(Q(admin__valid_nomination=True) | Q(admin__isnull=True))
)


class Nomination(models.Model):
class Meta:
permissions = [
Expand Down Expand Up @@ -349,6 +358,10 @@ def pretty_fields(self) -> str:
def __str__(self):
return f"{self.category} by {self.nominator.display_name} on {self.nomination_date}"

# make sure we have the objects manager
objects = models.Manager()
valid = NominationValidManager()


class NominationAdminData(models.Model):
nomination = models.OneToOneField(
Expand Down
40 changes: 27 additions & 13 deletions src/nomnom/nominate/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,33 @@

@receiver(m2m_changed, sender=Group.user_set.through)
def user_added_or_removed_from_group(sender, instance, action, pk_set, **kwargs):
reverse = kwargs.get("reverse", False)
if action == "post_remove":
try:
instance.convention_profile
except NominatingMemberProfile.DoesNotExist:
# no member for this, so we don't have nominations to remove
return

groups = Group.objects.filter(pk__in=pk_set)
convention_configuration = svcs_from().get(ConventionConfiguration)
nominating_group = convention_configuration.nominating_group

if reverse:
# removing from the group side, we find the nominators via the PKs and clean those up
if instance.name == nominating_group:
nominators = NominatingMemberProfile.objects.filter(user__pk__in=pk_set)
admin.set_validation(
Nomination.objects.filter(nominator__in=nominators),
False,
)

else:
try:
instance.convention_profile
except NominatingMemberProfile.DoesNotExist:
# no member for this, so we don't have nominations to remove
return

groups = Group.objects.filter(pk__in=pk_set)

if any(g.name == convention_configuration.nominating_group for g in groups):
# we've removed the user from the nominating group; we are going to invalidate all the
# user's nominations now.
admin.set_validation(
Nomination.objects.filter(nominator=instance.convention_profile), False
)
if any(g.name == nominating_group for g in groups):
# we've removed the user from the nominating group; we are going to invalidate all the
# user's nominations now.
admin.set_validation(
Nomination.objects.filter(nominator=instance.convention_profile),
False,
)
67 changes: 65 additions & 2 deletions src/nomnom/nominate/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import pytest
from django.contrib.auth.models import AnonymousUser, Permission
from django.contrib.auth.models import AnonymousUser, Group, Permission
from django_svcs.apps import svcs_from

from nomnom.convention import ConventionConfiguration
from nomnom.nominate.factories import (
CategoryFactory,
ElectionFactory,
NominatingMemberProfileFactory,
NominationFactory,
)
from nomnom.nominate.models import Election, Nomination

pytestmark = pytest.mark.usefixtures("db")


@pytest.fixture(name="nominating_group")
def make_nominating_group(db):
convention_configuration = svcs_from().get(ConventionConfiguration)
return Group.objects.get_or_create(name=convention_configuration.nominating_group)[
0
]


@pytest.fixture(name="voting_group")
def make_voting_group(db):
convention_configuration = svcs_from().get(ConventionConfiguration)
return Group.objects.get_or_create(name=convention_configuration.voting_group)[0]


@pytest.fixture(name="nominator")
def make_nominator():
def make_nominator(nominating_group):
return NominatingMemberProfileFactory()


Expand Down Expand Up @@ -138,3 +156,48 @@ def test_pretty_state_for_general_user(self):

def test_description_for_general_user(self):
assert self.election.describe_state(self.user) == "Nominations are open"


@pytest.fixture(name="set_of_nominations")
def make_set_of_nominations(election, nominator):
c1 = CategoryFactory.create(
election=election,
fields=2,
ballot_position=1,
)
other_member = NominatingMemberProfileFactory.create()
NominationFactory.create_batch(
2,
category=c1,
nominator=other_member,
)
NominationFactory.create_batch(
2,
category=c1,
nominator=nominator,
)


@pytest.mark.django_db
@pytest.mark.usefixtures("set_of_nominations")
def test_removing_nomination_permissions_via_user_invalidates_nominations(
nominator, nominating_group
):
nominator.user.groups.remove(nominating_group)
nominator.save()

# check our results
assert Nomination.valid.filter(nominator=nominator).count() == 0
assert Nomination.valid.count() == 2


@pytest.mark.django_db
@pytest.mark.usefixtures("set_of_nominations")
def test_removing_nomination_permissions_via_group_invalidates_nominations(
nominator, nominating_group: Group
):
nominating_group.user_set.remove(nominator.user)
nominator.save()

assert Nomination.valid.filter(nominator=nominator).count() == 0
assert Nomination.valid.count() == 2
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 235e702

Please sign in to comment.