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

Async team deletion with email #1169

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
18 changes: 17 additions & 1 deletion apps/teams/tasks.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
from celery import shared_task

from apps.teams.invitations import send_invitation_accepted
from apps.teams.models import Invitation
from apps.teams.models import Invitation, Team
from apps.utils.deletion import (
delete_object_with_auditing_of_related_objects,
get_admin_emails_with_delete_permission,
send_team_deleted_notification,
)


@shared_task(ignore_result=True)
def send_invitation_accepted_notification(invitation_id):
invitation = Invitation.objects.get(id=invitation_id)
send_invitation_accepted(invitation)


@shared_task
def delete_team_async(team_id):
team = Team.objects.get(id=team_id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use with current_team(team) to set the team for auditing


# get required info for emails prior to deleting team info
team_name = team.name
admin_emails = get_admin_emails_with_delete_permission(team)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all users maybe be notified instead of only those that can delete the team? It might be a bit surprising for someone to find their team is gone. Probably an edge case, but still

delete_object_with_auditing_of_related_objects(team)
send_team_deleted_notification(team_name, admin_emails)
2 changes: 2 additions & 0 deletions apps/teams/tests/test_creation_deletion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from django.test import override_settings
from django.urls import reverse
from field_audit.models import AuditEvent

Expand Down Expand Up @@ -69,6 +70,7 @@ def test_group_owner_assignment_on_team_creation(client):


@pytest.mark.django_db()
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
def test_delete_team(client, team_with_users):
client.force_login(team_with_users.members.first())
response = client.post(reverse("single_team:delete_team", args=[team_with_users.slug]))
Expand Down
18 changes: 17 additions & 1 deletion apps/teams/tests/test_get_admin_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from apps.teams.invitations import get_admin_emails
from apps.teams.utils import set_current_team
from apps.users.models import CustomUser
from apps.utils.deletion import get_admin_emails_with_delete_permission
from apps.utils.factories.team import MembershipFactory


@pytest.mark.django_db()
def test_get_admin_emails(team_with_users):
def test_get_admin_emails_can_view_invite(team_with_users):
set_current_team(team_with_users)

MembershipFactory(team=team_with_users, groups=lambda: list(Group.objects.filter(name=TEAM_ADMIN_GROUP)))
Expand All @@ -21,3 +22,18 @@ def test_get_admin_emails(team_with_users):
}
emails = get_admin_emails(team_with_users)
assert set(emails) == expected


@pytest.mark.django_db()
def test_get_admin_emails_can_delete_team(team_with_users):
set_current_team(team_with_users)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could use the current_team context manager to ensure that it is unset


MembershipFactory(team=team_with_users, groups=lambda: list(Group.objects.filter(name=TEAM_ADMIN_GROUP)))

expected = {
user.email
for user in CustomUser.objects.filter(membership__team=team_with_users).all()
if user.has_perm("teams.delete_team")
}
emails = get_admin_emails_with_delete_permission(team_with_users)
assert set(emails) == expected
11 changes: 8 additions & 3 deletions apps/teams/views/manage_team_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from apps.teams.forms import InvitationForm, TeamChangeForm
from apps.teams.invitations import send_invitation
from apps.teams.models import Invitation
from apps.teams.tasks import delete_team_async
from apps.teams.utils import current_team
from apps.utils.deletion import delete_object_with_auditing_of_related_objects
from apps.web.forms import set_form_fields_disabled


Expand Down Expand Up @@ -85,8 +85,13 @@ def create_team(request):
@require_POST
@permission_required("teams.delete_team", raise_exception=True)
def delete_team(request, team_slug):
delete_object_with_auditing_of_related_objects(request.team)
messages.success(request, _('The "{team}" team was successfully deleted').format(team=request.team.name))
delete_team_async.delay(request.team.id)
messages.success(
request,
_(
'The "{team}" team deletion process has started. An email will be sent to admins once it is complete.'
).format(team=request.team.name),
)
return HttpResponseRedirect(reverse("web:home"))


Expand Down
28 changes: 28 additions & 0 deletions apps/utils/deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
from operator import or_
from typing import Any

from django.conf import settings
from django.contrib.admin.utils import NestedObjects
from django.core.mail import send_mail
from django.db import models, router, transaction
from django.db.models import Expression, Q
from django.db.models.deletion import get_candidate_relations_to_delete
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from field_audit import field_audit
from field_audit.field_audit import get_audited_models

Expand Down Expand Up @@ -218,3 +222,27 @@ def get_related_pipelines_queryset(instance, pipeline_param_key: str = None):
Q(**{f"params__{pipeline_param_key}": instance.id}) | Q(**{f"params__{pipeline_param_key}": str(instance.id)})
)
return pipelines


def get_admin_emails_with_delete_permission(team):
from apps.teams.models import Membership

return list(
Membership.objects.filter(team__name=team.name, groups__permissions__codename="delete_team").values_list(
"user__email", flat=True
)
)


def send_team_deleted_notification(team_name, admin_emails):
email_context = {
"team_name": team_name,
}
send_mail(
subject=_("Team '{}' has been deleted").format(team_name),
message=render_to_string("teams/email/team_deleted_notification.txt", context=email_context),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=admin_emails,
fail_silently=False,
html_message=render_to_string("teams/email/domain_deleted_notification.html", context=email_context),
)
1 change: 1 addition & 0 deletions gpt_playground/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@
"account_reset_password_from_key",
"teams:signup_after_invite",
"account_login",
"single_team:delete_team",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

]
FIELD_AUDIT_REQUEST_ID_HEADERS = [
"X-Request-ID", # Heroku
Expand Down
3 changes: 3 additions & 0 deletions templates/teams/email/team_deleted_notification.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>
Team '{{ team_name }}' has been successfully deleted.
</p>
1 change: 1 addition & 0 deletions templates/teams/email/team_deleted_notification.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Team '{{ team_name }}' has been successfully deleted.