Repository: svthalia/concrexit

Issue:
Allow group memberships to end in the future
In GitLab by @joostrijneveld on Jan 30, 2017, 18:44

### One-sentence description

Allow group memberships to end in the future

### Why?

Because currently memberships of board members never really end until changed after the board year, and it could be possible for committee or society members to end their membership on a date that is already known and it would be nice to not have to worry about this anymore.

### Current implementation

`is_staff` is set to True for everyone who becomes a board member and is never set to False because board memberships don't end (until someone changes the value). This is also the reason why committee membership end dates cannot be in the future, since it sets `is_staff` to False if applicable.

### Desired implementation

Set it to False using a cronjob for example


Discussion:
In GitLab by @se-bastiaan on Sep 12, 2018, 19:48

We could create a periodic Celery task that removes sets is_staff to False for non-board/committee members or something.
In GitLab by @se-bastiaan on Oct 18, 2018, 11:33

(at)tvbussel @lukovdm @thomwiggers What do you think? Can we just decide to keep `is_staff` on `True`? I don't see any problems that could be caused. Those people won't have permissions if they're not in committees anyways.
In GitLab by @thomwiggers on Oct 18, 2018, 11:43

Board memberships do end now, right? Also, being able to set `end_date` for committee members seems desirable.

I would need to check how `is_staff` is determined in the user model again.
In GitLab by @se-bastiaan on Oct 18, 2018, 11:44

Not yet, but after removing the constraint they can end indeed.
You mean setting that `end_date` on creation of the membership?
In GitLab by @thomwiggers on Oct 18, 2018, 11:46

We currently determine `is_staff` by setting it whenever we **change** a groupmembership.

        self.member.is_staff = (self.member
                                .membergroupmembership_set
                                .exclude(until__lte=timezone.now().date())
                                .count()) >= 1
        self.member.save()


So `Society` members are also staff. 

Also, setting `until` should be fine, but then someone isn't kicked out of `is_staff` status...

But we don't really rely on `is_staff` for much anyway,... right?
In GitLab by @se-bastiaan on Oct 18, 2018, 11:47

I'm pretty sure `is_staff` is only used by the django admin. We just use permissions to have more granular control.
In GitLab by @thomwiggers on Oct 18, 2018, 11:50

Ah, I think I reconstructed what happens:

> This is also the reason why committee membership end dates cannot be in the future, since it sets `is_staff` to False if applicable.

The front-end enforces that end-dates are not in the future, because it would allow the following

1. Make someone (committee) group member
2. They get `is_staff=True`
2. Make their membership end tomorrow
3. They'll never get `is_staff=False`.

This MR really asks for a cronjob that assigns `is_staff` based on *current* view of memberships of groups. Then we can relax these checks in the front-end and allow group memberships to end in the future.
In GitLab by @se-bastiaan on Oct 18, 2018, 12:42

I guess a cronjob is the best solution then. :thumbsup:
I think this can really be easy and fun, as it's basically just writing a cronjob
For making this more flexible, maybe we should overwrite the staff property in the django user

Resolving patch:
diff --git a/website/activemembers/management/commands/revoke_staff.py b/website/activemembers/management/commands/revoke_staff.py
new file mode 100644
--- /dev/null
+++ b/website/activemembers/management/commands/revoke_staff.py
@@ -0,0 +1,12 @@
+from django.core.management.base import BaseCommand
+
+from activemembers.services import revoke_staff_permission_for_users_in_no_commitee
+
+
+class Command(BaseCommand):
+    """This command can be executed daily to check group membership and revoke staff permissions when needed."""
+
+    def handle(self, *args, **options):
+        revoked = revoke_staff_permission_for_users_in_no_commitee()
+        for member in revoked:
+            self.stdout.write("Revoked staff permissions for {}".format(member))
diff --git a/website/activemembers/models.py b/website/activemembers/models.py
--- a/website/activemembers/models.py
+++ b/website/activemembers/models.py
@@ -292,8 +292,10 @@ def clean(self):
                 raise ValidationError(
                     {"until": _("End date can't be before start date")}
                 )
-            if self.until and self.until > timezone.now().date():
-                raise ValidationError({"until": _("End date can't be in the future")})
+            if self.until and self.group.until and self.until > self.group.until:
+                raise ValidationError(
+                    {"until": _("End date can't be after the group end date")}
+                )
             if self.since and self.group.since and self.since < self.group.since:
                 raise ValidationError(
                     {"since": _("Start date can't be before group start date")}
diff --git a/website/activemembers/services.py b/website/activemembers/services.py
--- a/website/activemembers/services.py
+++ b/website/activemembers/services.py
@@ -2,6 +2,7 @@
 from django.utils import timezone
 
 from activemembers.models import Committee
+from members.models.member import Member
 
 
 def generate_statistics() -> dict:
@@ -29,3 +30,14 @@ def generate_statistics() -> dict:
         data["datasets"][0]["data"].append(committee.member_count)
 
     return data
+
+
+def revoke_staff_permission_for_users_in_no_commitee():
+    members = Member.objects.filter(is_staff=True)
+    revoked = []
+    for member in members:
+        if member.get_member_groups().count() == 0:
+            revoked.append(member.id)
+            member.is_staff = False
+            member.save()
+    return Member.objects.filter(pk__in=revoked)


Test patch: