replace MetaGroups by proper group management

This commit is contained in:
imperosol
2024-11-30 20:30:17 +01:00
parent cce7ecbe73
commit 6400b2c2c2
16 changed files with 410 additions and 355 deletions

View File

@ -23,8 +23,8 @@ admin.site.unregister(AuthGroup)
@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
list_display = ("name", "description", "is_meta")
list_filter = ("is_meta",)
list_display = ("name", "description", "is_manually_manageable")
list_filter = ("is_manually_manageable",)
search_fields = ("name",)

View File

@ -7,7 +7,7 @@ from model_bakery import seq
from model_bakery.recipe import Recipe, related
from club.models import Membership
from core.models import User
from core.models import Group, User
from subscription.models import Subscription
active_subscription = Recipe(
@ -60,5 +60,6 @@ board_user = Recipe(
first_name="AE",
last_name=seq("member "),
memberships=related(ae_board_membership),
groups=lambda: [Group.objects.get(club_board=settings.SITH_MAIN_CLUB_ID)],
)
"""A user which is in the board of the AE."""

View File

@ -47,7 +47,7 @@ from accounting.models import (
)
from club.models import Club, Membership
from com.models import News, NewsDate, Sith, Weekmail
from core.models import Group, Page, PageRev, RealGroup, SithFile, User
from core.models import Group, Page, PageRev, SithFile, User
from core.utils import resize_image
from counter.models import Counter, Product, ProductType, StudentCard
from election.models import Candidature, Election, ElectionList, Role
@ -143,7 +143,9 @@ class Command(BaseCommand):
Counter.objects.bulk_create(counters)
bar_groups = []
for bar_id, bar_name in settings.SITH_COUNTER_BARS:
group = RealGroup.objects.create(name=f"{bar_name} admin")
group = Group.objects.create(
name=f"{bar_name} admin", is_manually_manageable=True
)
bar_groups.append(
Counter.edit_groups.through(counter_id=bar_id, group=group)
)
@ -366,46 +368,42 @@ Welcome to the wiki page!
parent=main_club,
)
Membership.objects.bulk_create(
[
Membership(user=skia, club=main_club, role=3),
Membership(
user=comunity,
club=bar_club,
start_date=localdate(),
role=settings.SITH_CLUB_ROLES_ID["Board member"],
),
Membership(
user=sli,
club=troll,
role=9,
description="Padawan Troll",
start_date=localdate() - timedelta(days=17),
),
Membership(
user=krophil,
club=troll,
role=10,
description="Maitre Troll",
start_date=localdate() - timedelta(days=200),
),
Membership(
user=skia,
club=troll,
role=2,
description="Grand Ancien Troll",
start_date=localdate() - timedelta(days=400),
end_date=localdate() - timedelta(days=86),
),
Membership(
user=richard,
club=troll,
role=2,
description="",
start_date=localdate() - timedelta(days=200),
end_date=localdate() - timedelta(days=100),
),
]
Membership.objects.create(user=skia, club=main_club, role=3)
Membership.objects.create(
user=comunity,
club=bar_club,
start_date=localdate(),
role=settings.SITH_CLUB_ROLES_ID["Board member"],
)
Membership.objects.create(
user=sli,
club=troll,
role=9,
description="Padawan Troll",
start_date=localdate() - timedelta(days=17),
)
Membership.objects.create(
user=krophil,
club=troll,
role=10,
description="Maitre Troll",
start_date=localdate() - timedelta(days=200),
)
Membership.objects.create(
user=skia,
club=troll,
role=2,
description="Grand Ancien Troll",
start_date=localdate() - timedelta(days=400),
end_date=localdate() - timedelta(days=86),
)
Membership.objects.create(
user=richard,
club=troll,
role=2,
description="",
start_date=localdate() - timedelta(days=200),
end_date=localdate() - timedelta(days=100),
)
p = ProductType.objects.create(name="Bières bouteilles")
@ -594,7 +592,6 @@ Welcome to the wiki page!
)
# Create an election
ae_board_group = Group.objects.get(name=settings.SITH_MAIN_BOARD_GROUP)
el = Election.objects.create(
title="Élection 2017",
description="La roue tourne",
@ -604,7 +601,7 @@ Welcome to the wiki page!
end_date="7942-06-12 10:28:45+01",
)
el.view_groups.add(groups.public)
el.edit_groups.add(ae_board_group)
el.edit_groups.add(main_club.board_group)
el.candidature_groups.add(groups.subscribers)
el.vote_groups.add(groups.subscribers)
liste = ElectionList.objects.create(title="Candidature Libre", election=el)
@ -889,7 +886,7 @@ Welcome to the wiki page!
def _create_groups(self) -> PopulatedGroups:
perms = Permission.objects.all()
root_group = Group.objects.create(name="Root")
root_group = Group.objects.create(name="Root", is_manually_manageable=True)
root_group.permissions.add(*list(perms.values_list("pk", flat=True)))
# public has no permission.
# Its purpose is not to link users to permissions,
@ -911,7 +908,9 @@ Welcome to the wiki page!
)
)
)
accounting_admin = Group.objects.create(name="Accounting admin")
accounting_admin = Group.objects.create(
name="Accounting admin", is_manually_manageable=True
)
accounting_admin.permissions.add(
*list(
perms.filter(
@ -931,13 +930,17 @@ Welcome to the wiki page!
).values_list("pk", flat=True)
)
)
com_admin = Group.objects.create(name="Communication admin")
com_admin = Group.objects.create(
name="Communication admin", is_manually_manageable=True
)
com_admin.permissions.add(
*list(
perms.filter(content_type__app_label="com").values_list("pk", flat=True)
)
)
counter_admin = Group.objects.create(name="Counter admin")
counter_admin = Group.objects.create(
name="Counter admin", is_manually_manageable=True
)
counter_admin.permissions.add(
*list(
perms.filter(
@ -946,16 +949,20 @@ Welcome to the wiki page!
)
)
)
Group.objects.create(name="Banned from buying alcohol")
Group.objects.create(name="Banned from counters")
Group.objects.create(name="Banned to subscribe")
sas_admin = Group.objects.create(name="SAS admin")
Group.objects.create(
name="Banned from buying alcohol", is_manually_manageable=True
)
Group.objects.create(name="Banned from counters", is_manually_manageable=True)
Group.objects.create(name="Banned to subscribe", is_manually_manageable=True)
sas_admin = Group.objects.create(name="SAS admin", is_manually_manageable=True)
sas_admin.permissions.add(
*list(
perms.filter(content_type__app_label="sas").values_list("pk", flat=True)
)
)
forum_admin = Group.objects.create(name="Forum admin")
forum_admin = Group.objects.create(
name="Forum admin", is_manually_manageable=True
)
forum_admin.permissions.add(
*list(
perms.filter(content_type__app_label="forum").values_list(
@ -963,7 +970,9 @@ Welcome to the wiki page!
)
)
)
pedagogy_admin = Group.objects.create(name="Pedagogy admin")
pedagogy_admin = Group.objects.create(
name="Pedagogy admin", is_manually_manageable=True
)
pedagogy_admin.permissions.add(
*list(
perms.filter(content_type__app_label="pedagogy").values_list(

View File

@ -563,14 +563,21 @@ class Migration(migrations.Migration):
fields=[],
options={"proxy": True},
bases=("core.group",),
managers=[("objects", core.models.MetaGroupManager())],
managers=[("objects", django.contrib.auth.models.GroupManager())],
),
# at first, there existed a RealGroupManager and a RealGroupManager,
# which have been since been removed.
# However, this removal broke the migrations because it caused an ImportError.
# Thus, the managers MetaGroupManager (above) and RealGroupManager (below)
# have been replaced by the base django GroupManager to fix the import.
# As those managers aren't actually used in migrations,
# this replacement doesn't break anything.
migrations.CreateModel(
name="RealGroup",
fields=[],
options={"proxy": True},
bases=("core.group",),
managers=[("objects", core.models.RealGroupManager())],
managers=[("objects", django.contrib.auth.models.GroupManager())],
),
migrations.AlterUniqueTogether(
name="page", unique_together={("name", "parent")}

View File

@ -0,0 +1,51 @@
# Generated by Django 4.2.16 on 2024-11-30 13:16
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import F
def invert_is_manually_manageable(apps: StateApps, schema_editor):
"""Invert `is_manually_manageable`.
This field is a renaming of `is_meta`.
However, the meaning has been inverted : the groups
which were meta are not manually manageable and vice versa.
Thus, the value must be inverted.
"""
Group = apps.get_model("core", "Group")
Group.objects.all().update(is_manually_manageable=~F("is_manually_manageable"))
class Migration(migrations.Migration):
dependencies = [("core", "0040_alter_user_options_user_user_permissions_and_more")]
operations = [
migrations.DeleteModel(
name="MetaGroup",
),
migrations.DeleteModel(
name="RealGroup",
),
migrations.AlterModelOptions(
name="group",
options={},
),
migrations.RenameField(
model_name="group",
old_name="is_meta",
new_name="is_manually_manageable",
),
migrations.AlterField(
model_name="group",
name="is_manually_manageable",
field=models.BooleanField(
default=False,
help_text="If False, this shouldn't be shown on group management pages",
verbose_name="Is manually manageable",
),
),
migrations.RunPython(
invert_is_manually_manageable, reverse_code=invert_is_manually_manageable
),
]

View File

@ -36,7 +36,6 @@ from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.models import AnonymousUser as AuthAnonymousUser
from django.contrib.auth.models import Group as AuthGroup
from django.contrib.auth.models import GroupManager as AuthGroupManager
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core import validators
from django.core.cache import cache
@ -58,34 +57,17 @@ if TYPE_CHECKING:
from club.models import Club
class RealGroupManager(AuthGroupManager):
def get_queryset(self):
return super().get_queryset().filter(is_meta=False)
class MetaGroupManager(AuthGroupManager):
def get_queryset(self):
return super().get_queryset().filter(is_meta=True)
class Group(AuthGroup):
"""Implement both RealGroups and Meta groups.
"""Wrapper around django.auth.Group"""
Groups are sorted by their is_meta property
"""
#: If False, this is a RealGroup
is_meta = models.BooleanField(
_("meta group status"),
is_manually_manageable = models.BooleanField(
_("Is manually manageable"),
default=False,
help_text=_("Whether a group is a meta group or not"),
help_text=_("If False, this shouldn't be shown on group management pages"),
)
#: Description of the group
description = models.CharField(_("description"), max_length=60)
class Meta:
ordering = ["name"]
def get_absolute_url(self) -> str:
return reverse("core:group_list")
@ -100,65 +82,6 @@ class Group(AuthGroup):
cache.delete(f"sith_group_{self.name.replace(' ', '_')}")
class MetaGroup(Group):
"""MetaGroups are dynamically created groups.
Generally used with clubs where creating a club creates two groups:
* club-SITH_BOARD_SUFFIX
* club-SITH_MEMBER_SUFFIX
"""
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=False
objects = MetaGroupManager()
class Meta:
proxy = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_meta = True
@cached_property
def associated_club(self) -> Club | None:
"""Return the group associated with this meta group.
The result of this function is cached
Returns:
The associated club if it exists, else None
"""
from club.models import Club
if self.name.endswith(settings.SITH_BOARD_SUFFIX):
# replace this with str.removesuffix as soon as Python
# is upgraded to 3.10
club_name = self.name[: -len(settings.SITH_BOARD_SUFFIX)]
elif self.name.endswith(settings.SITH_MEMBER_SUFFIX):
club_name = self.name[: -len(settings.SITH_MEMBER_SUFFIX)]
else:
return None
club = cache.get(f"sith_club_{club_name}")
if club is None:
club = Club.objects.filter(unix_name=club_name).first()
cache.set(f"sith_club_{club_name}", club)
return club
class RealGroup(Group):
"""RealGroups are created by the developer.
Most of the time they match a number in settings to be easily used for permissions.
"""
#: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=True
objects = RealGroupManager()
class Meta:
proxy = True
def validate_promo(value: int) -> None:
start_year = settings.SITH_SCHOOL_START_YEAR
delta = (localdate() + timedelta(days=180)).year - start_year
@ -204,8 +127,8 @@ def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None
else:
group = Group.objects.filter(name=name).first()
if group is not None:
cache.set(f"sith_group_{group.id}", group)
cache.set(f"sith_group_{group.name.replace(' ', '_')}", group)
name = group.name.replace(" ", "_")
cache.set_many({f"sith_group_{group.id}": group, f"sith_group_{name}": group})
else:
cache.set(f"sith_group_{pk_or_name}", "not_found")
return group
@ -438,18 +361,6 @@ class User(AbstractUser):
return self.was_subscribed
if group.id == settings.SITH_GROUP_ROOT_ID:
return self.is_root
if group.is_meta:
# check if this group is associated with a club
group.__class__ = MetaGroup
club = group.associated_club
if club is None:
return False
membership = club.get_membership_for(self)
if membership is None:
return False
if group.name.endswith(settings.SITH_MEMBER_SUFFIX):
return True
return membership.role > settings.SITH_MAXIMUM_FREE_ROLE
return group in self.cached_groups
@property
@ -474,12 +385,11 @@ class User(AbstractUser):
return any(g.id == root_id for g in self.cached_groups)
@cached_property
def is_board_member(self):
main_club = settings.SITH_MAIN_CLUB["unix_name"]
return self.is_in_group(name=main_club + settings.SITH_BOARD_SUFFIX)
def is_board_member(self) -> bool:
return self.groups.filter(club_board=settings.SITH_MAIN_CLUB_ID).exists()
@cached_property
def can_read_subscription_history(self):
def can_read_subscription_history(self) -> bool:
if self.is_root or self.is_board_member:
return True
@ -943,7 +853,7 @@ class SithFile(models.Model):
param="1",
).save()
def is_owned_by(self, user):
def is_owned_by(self, user: User) -> bool:
if user.is_anonymous:
return False
if user.is_root:
@ -958,7 +868,7 @@ class SithFile(models.Model):
return True
return user.id == self.owner_id
def can_be_viewed_by(self, user):
def can_be_viewed_by(self, user: User) -> bool:
if hasattr(self, "profile_of"):
return user.can_view(self.profile_of)
if hasattr(self, "avatar_of"):

View File

@ -18,6 +18,7 @@ from smtplib import SMTPException
import freezegun
import pytest
from django.contrib.auth.hashers import make_password
from django.core import mail
from django.core.cache import cache
from django.core.mail import EmailMessage
@ -30,7 +31,7 @@ from model_bakery import baker
from pytest_django.asserts import assertInHTML, assertRedirects
from antispam.models import ToxicDomain
from club.models import Membership
from club.models import Club, Membership
from core.markdown import markdown
from core.models import AnonymousUser, Group, Page, User
from core.utils import get_semester_code, get_start_of_semester
@ -145,7 +146,7 @@ class TestUserRegistration:
class TestUserLogin:
@pytest.fixture()
def user(self) -> User:
return User.objects.first()
return baker.make(User, password=make_password("plop"))
def test_login_fail(self, client, user):
"""Should not login a user correctly."""
@ -349,14 +350,9 @@ class TestUserIsInGroup(TestCase):
@classmethod
def setUpTestData(cls):
from club.models import Club
cls.root_group = Group.objects.get(name="Root")
cls.public = Group.objects.get(name="Public")
cls.skia = User.objects.get(username="skia")
cls.toto = User.objects.create(
username="toto", first_name="a", last_name="b", email="a.b@toto.fr"
)
cls.public_group = Group.objects.get(name="Public")
cls.public_user = baker.make(User)
cls.subscribers = Group.objects.get(name="Subscribers")
cls.old_subscribers = Group.objects.get(name="Old subscribers")
cls.accounting_admin = Group.objects.get(name="Accounting admin")
@ -366,22 +362,12 @@ class TestUserIsInGroup(TestCase):
cls.banned_counters = Group.objects.get(name="Banned from counters")
cls.banned_subscription = Group.objects.get(name="Banned to subscribe")
cls.sas_admin = Group.objects.get(name="SAS admin")
cls.club = Club.objects.create(
name="Fake Club",
unix_name="fake-club",
address="Fake address",
)
cls.club = baker.make(Club)
cls.main_club = Club.objects.get(id=1)
def assert_in_public_group(self, user):
assert user.is_in_group(pk=self.public.id)
assert user.is_in_group(name=self.public.name)
def assert_in_club_metagroups(self, user, club):
meta_groups_board = club.unix_name + settings.SITH_BOARD_SUFFIX
meta_groups_members = club.unix_name + settings.SITH_MEMBER_SUFFIX
assert user.is_in_group(name=meta_groups_board) is False
assert user.is_in_group(name=meta_groups_members) is False
assert user.is_in_group(pk=self.public_group.id)
assert user.is_in_group(name=self.public_group.name)
def assert_only_in_public_group(self, user):
self.assert_in_public_group(user)
@ -392,13 +378,11 @@ class TestUserIsInGroup(TestCase):
self.sas_admin,
self.subscribers,
self.old_subscribers,
self.club.members_group,
self.club.board_group,
):
assert not user.is_in_group(pk=group.pk)
assert not user.is_in_group(name=group.name)
meta_groups_board = self.club.unix_name + settings.SITH_BOARD_SUFFIX
meta_groups_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
assert user.is_in_group(name=meta_groups_board) is False
assert user.is_in_group(name=meta_groups_members) is False
def test_anonymous_user(self):
"""Test that anonymous users are only in the public group."""
@ -407,80 +391,80 @@ class TestUserIsInGroup(TestCase):
def test_not_subscribed_user(self):
"""Test that users who never subscribed are only in the public group."""
self.assert_only_in_public_group(self.toto)
self.assert_only_in_public_group(self.public_user)
def test_wrong_parameter_fail(self):
"""Test that when neither the pk nor the name argument is given,
the function raises a ValueError.
"""
with self.assertRaises(ValueError):
self.toto.is_in_group()
self.public_user.is_in_group()
def test_number_queries(self):
"""Test that the number of db queries is stable
and that less queries are made when making a new call.
"""
# make sure Skia is in at least one group
self.skia.groups.add(Group.objects.first().pk)
skia_groups = self.skia.groups.all()
group_in = baker.make(Group)
self.public_user.groups.add(group_in)
group_in = skia_groups.first()
cache.clear()
# Test when the user is in the group
with self.assertNumQueries(2):
self.skia.is_in_group(pk=group_in.id)
self.public_user.is_in_group(pk=group_in.id)
with self.assertNumQueries(0):
self.skia.is_in_group(pk=group_in.id)
self.public_user.is_in_group(pk=group_in.id)
ids = skia_groups.values_list("pk", flat=True)
group_not_in = Group.objects.exclude(pk__in=ids).first()
group_not_in = baker.make(Group)
cache.clear()
# Test when the user is not in the group
with self.assertNumQueries(2):
self.skia.is_in_group(pk=group_not_in.id)
self.public_user.is_in_group(pk=group_not_in.id)
with self.assertNumQueries(0):
self.skia.is_in_group(pk=group_not_in.id)
self.public_user.is_in_group(pk=group_not_in.id)
def test_cache_properly_cleared_membership(self):
"""Test that when the membership of a user end,
the cache is properly invalidated.
"""
membership = Membership.objects.create(
club=self.club, user=self.toto, end_date=None
)
meta_groups_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX
membership = baker.make(Membership, club=self.club, user=self.public_user)
cache.clear()
assert self.toto.is_in_group(name=meta_groups_members) is True
assert membership == cache.get(f"membership_{self.club.id}_{self.toto.id}")
self.club.get_membership_for(self.public_user) # this should populate the cache
assert membership == cache.get(
f"membership_{self.club.id}_{self.public_user.id}"
)
membership.end_date = now() - timedelta(minutes=5)
membership.save()
cached_membership = cache.get(f"membership_{self.club.id}_{self.toto.id}")
cached_membership = cache.get(
f"membership_{self.club.id}_{self.public_user.id}"
)
assert cached_membership == "not_member"
assert self.toto.is_in_group(name=meta_groups_members) is False
def test_cache_properly_cleared_group(self):
"""Test that when a user is removed from a group,
the is_in_group_method return False when calling it again.
"""
# testing with pk
self.toto.groups.add(self.com_admin.pk)
assert self.toto.is_in_group(pk=self.com_admin.pk) is True
self.public_user.groups.add(self.com_admin.pk)
assert self.public_user.is_in_group(pk=self.com_admin.pk) is True
self.toto.groups.remove(self.com_admin.pk)
assert self.toto.is_in_group(pk=self.com_admin.pk) is False
self.public_user.groups.remove(self.com_admin.pk)
assert self.public_user.is_in_group(pk=self.com_admin.pk) is False
# testing with name
self.toto.groups.add(self.sas_admin.pk)
assert self.toto.is_in_group(name="SAS admin") is True
self.public_user.groups.add(self.sas_admin.pk)
assert self.public_user.is_in_group(name="SAS admin") is True
self.toto.groups.remove(self.sas_admin.pk)
assert self.toto.is_in_group(name="SAS admin") is False
self.public_user.groups.remove(self.sas_admin.pk)
assert self.public_user.is_in_group(name="SAS admin") is False
def test_not_existing_group(self):
"""Test that searching for a not existing group
returns False.
"""
assert self.skia.is_in_group(name="This doesn't exist") is False
user = baker.make(User)
user.groups.set(list(Group.objects.all()))
assert not user.is_in_group(name="This doesn't exist")
class TestDateUtils(TestCase):

View File

@ -32,19 +32,14 @@ from django.contrib.staticfiles.management.commands.collectstatic import (
)
from django.core.exceptions import ValidationError
from django.db import transaction
from django.forms import (
CheckboxSelectMultiple,
DateInput,
DateTimeInput,
TextInput,
)
from django.forms import CheckboxSelectMultiple, DateInput, DateTimeInput, TextInput
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from PIL import Image
from antispam.forms import AntiSpamEmailField
from core.models import Gift, Page, RealGroup, SithFile, User
from core.models import Gift, Group, Page, SithFile, User
from core.utils import resize_image
from core.views.widgets.select import (
AutoCompleteSelect,
@ -293,7 +288,7 @@ class UserGroupsForm(forms.ModelForm):
required_css_class = "required"
groups = forms.ModelMultipleChoiceField(
queryset=RealGroup.objects.all(),
queryset=Group.objects.filter(is_manually_manageable=True),
widget=CheckboxSelectMultiple,
label=_("Groups"),
required=False,

View File

@ -21,7 +21,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from core.models import RealGroup, User
from core.models import Group, User
from core.views import CanCreateMixin, CanEditMixin, DetailFormView
from core.views.widgets.select import AutoCompleteSelectMultipleUser
@ -57,7 +57,8 @@ class EditMembersForm(forms.Form):
class GroupListView(CanEditMixin, ListView):
"""Displays the Group list."""
model = RealGroup
model = Group
queryset = Group.objects.filter(is_manually_manageable=True)
ordering = ["name"]
template_name = "core/group_list.jinja"
@ -65,7 +66,8 @@ class GroupListView(CanEditMixin, ListView):
class GroupEditView(CanEditMixin, UpdateView):
"""Edit infos of a Group."""
model = RealGroup
model = Group
queryset = Group.objects.filter(is_manually_manageable=True)
pk_url_kwarg = "group_id"
template_name = "core/group_edit.jinja"
fields = ["name", "description"]
@ -74,7 +76,8 @@ class GroupEditView(CanEditMixin, UpdateView):
class GroupCreateView(CanCreateMixin, CreateView):
"""Add a new Group."""
model = RealGroup
model = Group
queryset = Group.objects.filter(is_manually_manageable=True)
template_name = "core/create.jinja"
fields = ["name", "description"]
@ -84,7 +87,8 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
Allow adding and removing users from it.
"""
model = RealGroup
model = Group
queryset = Group.objects.filter(is_manually_manageable=True)
form_class = EditMembersForm
pk_url_kwarg = "group_id"
template_name = "core/group_detail.jinja"
@ -118,7 +122,8 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
class GroupDeleteView(CanEditMixin, DeleteView):
"""Delete a Group."""
model = RealGroup
model = Group
queryset = Group.objects.filter(is_manually_manageable=True)
pk_url_kwarg = "group_id"
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("core:group_list")