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

Fix/remove user/soft removal #1724

Merged
merged 7 commits into from
Jan 16, 2025
Merged
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
2 changes: 1 addition & 1 deletion src/apps/api/views/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ class CompetitionParticipantViewSet(ModelViewSet):
queryset = CompetitionParticipant.objects.all()
serializer_class = CompetitionParticipantSerializer
filter_backends = (DjangoFilterBackend, SearchFilter)
filter_fields = ('user__username', 'user__email', 'status', 'competition')
filter_fields = ('user__username', 'user__email', 'status', 'competition', 'user__is_deleted')
search_fields = ('user__username', 'user__email',)

def get_queryset(self):
Expand Down
2 changes: 1 addition & 1 deletion src/apps/api/views/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def delete_account(request):
is_username_valid = user.username == request.data["username"]
is_password_valid = user.check_password(request.data["password"])

if(is_username_valid and is_password_valid):
if is_username_valid and is_password_valid:
send_delete_account_confirmation_mail(request, user)

return Response({
Expand Down
1 change: 1 addition & 0 deletions src/apps/profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def delete(self, *args, **kwargs):
# Mark the user as deleted
self.is_deleted = True
self.deleted_at = now()
self.is_active = False

# Anonymize or removed personal data
user_email = self.email # keep track of the email for the end of the procedure
Expand Down
18 changes: 17 additions & 1 deletion src/apps/profiles/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@

class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
return (six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active))
return (
six.text_type(user.pk) +
six.text_type(timestamp) +
six.text_type(user.is_active)
)


account_activation_token = AccountActivationTokenGenerator()


class AccountDeletionTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
return (
six.text_type(user.pk) +
six.text_type(timestamp) +
six.text_type(user.is_deleted)
)


account_deletion_token = AccountDeletionTokenGenerator()
61 changes: 35 additions & 26 deletions src/apps/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from django.shortcuts import render, redirect
from django.contrib.auth import views as auth_views
from django.contrib.auth import forms as auth_forms
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.mixins import LoginRequiredMixin
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
Expand All @@ -23,11 +22,12 @@
from .forms import SignUpForm, LoginForm, ActivationForm
from .models import User, Organization, Membership
from oidc_configurations.models import Auth_Organization
from .tokens import account_activation_token
from .tokens import account_activation_token, account_deletion_token
from competitions.models import Competition
from datasets.models import Data, DataGroup
from tasks.models import Task
from forums.models import Post
from utils.email import codalab_send_mail


class LoginView(auth_views.LoginView):
Expand Down Expand Up @@ -110,20 +110,21 @@ def activateEmail(request, user, to_email):


def send_delete_account_confirmation_mail(request, user):
mail_subject = 'Confirm Your Account Deletion Request'
message = render_to_string('profiles/emails/template_delete_account.html', {
context = {
'user': user,
'domain': get_current_site(request).domain,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'token': default_token_generator.make_token(user),
'token': account_deletion_token.make_token(user),
'protocol': 'https' if request.is_secure() else 'http'
})
email = EmailMessage(mail_subject, message, to=[user.email])
if email.send():
messages.success(request, f'Dear {user.username}, please go to your email inbox and click on \
the link to complete the deletion process. *Note: Check your spam folder.')
else:
messages.error(request, f'Problem sending confirmation email.')
}
codalab_send_mail(
context_data=context,
subject=f'Confirm Your Account Deletion Request',
html_file="profiles/emails/template_delete_account.html",
text_file="profiles/emails/template_delete_account.txt",
to_email=[user.email]
)
messages.success(request, f'Dear {user.username}, please go to your email inbox and click on the link to complete the deletion process. *Note: Check your spam folder.')


def send_user_deletion_notice_to_admin(user):
Expand All @@ -139,10 +140,11 @@ def send_user_deletion_notice_to_admin(user):
tasks = Task.objects.filter(created_by=user)
queues = user.queues.all()
posts = Post.objects.filter(posted_by=user)
deleted_user = user

mail_subject = f'Notice: user {user.username} removed his account'
message = render_to_string('profiles/emails/template_delete_account_notice.html', {
'user': user,
context = {
ihsaan-ullah marked this conversation as resolved.
Show resolved Hide resolved
'deleted_user': user,
'user': "",
'organizations': organizations,
'competitions_organizer': competitions_organizer,
'competitions_participation': competitions_participation,
Expand All @@ -152,16 +154,24 @@ def send_user_deletion_notice_to_admin(user):
'tasks': tasks,
'queues': queues,
'posts': posts
})
email = EmailMessage(mail_subject, message, to=admin_emails)
email.send()
}
codalab_send_mail(
context_data=context,
subject=f'Notice: user {deleted_user.username} removed his account',
html_file="profiles/emails/template_delete_account_notice.html",
text_file="profiles/emails/template_delete_account_notice.txt",
to_email=admin_emails
)


def send_user_deletion_confirmed(email):
mail_subject = f'Codabench: your account has been successfully removed'
message = render_to_string('profiles/emails/template_delete_account_confirmed.html')
email = EmailMessage(mail_subject, message, to=[email])
email.send()
codalab_send_mail(
context_data={},
subject=f'Codabench: your account has been successfully removed',
html_file="profiles/emails/template_delete_account_confirmed.html",
text_file="profiles/emails/template_delete_account_confirmed.txt",
to_email=[email]
)


def delete(request, uidb64, token):
Expand All @@ -172,15 +182,14 @@ def delete(request, uidb64, token):
user = None
messages.error(request, f"User not found.")
return redirect('accounts:user_account')
if user is not None and default_token_generator.check_token(user, token):
if user is not None and account_deletion_token.check_token(user, token):
# Soft delete the user
user.delete()

messages.success(request, f'Your account has been removed !')
messages.success(request, f'Your account has been removed!')
return redirect('accounts:logout')
else:
messages.error(request, f"Confirmation link is invalid or expired.")
return redirect('accounts:user_account')
return redirect('pages:home')


def sign_up(request):
Expand Down
9 changes: 9 additions & 0 deletions src/static/riot/competitions/detail/participant_manager.tag
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<option value="denied">Denied</option>
<option value="unknown">Unknown</option>
</select>
<div class="ui checkbox">
<input type="checkbox" ref="participant_show_deleted" onchange="{ update_participants.bind(this, undefined) }">
<label>Show deleted accounts</label>
</div>
<div class="ui blue icon button" onclick="{show_email_modal.bind(this, undefined)}"><i class="envelope icon"></i> Email all participants</div>
<table class="ui celled striped table">
<thead>
Expand Down Expand Up @@ -140,6 +144,11 @@
filters.status = status
}

let show_deleted_users = self.refs.participant_show_deleted.checked
if (show_deleted_users !== null && show_deleted_users === false) {
filters.user__is_deleted = show_deleted_users
}

CODALAB.api.get_participants(filters)
.done(participants => {
self.participants = participants
Expand Down
2 changes: 2 additions & 0 deletions src/static/riot/profiles/profile_account.tag
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
self.isDeleteAccountSubmitButtonDisabled = true;

self.show_modal = selector => $(selector).modal('show');
self.hide_modal = selector => $(selector).modal('hide');

self.checkFields = function() {
const formValues = $('#delete-account-form').form('get values');
Expand All @@ -80,6 +81,7 @@
const success = response.success;
if (success) {
toastr.success(response.message);
self.hide_modal('.delete-account.modal')
} else {
toastr.error(response.error);
}
Expand Down
24 changes: 24 additions & 0 deletions src/templates/profiles/emails/template_delete_account.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% extends 'emails/base_email.html' %}

{% block content %}
<p>We have received your request to delete your account.</p>
<p>To proceed with the deletion of your account, please confirm your request by clicking the link below:</p>
<p><a href="{{ protocol }}://{{ domain }}{% url 'accounts:delete' uidb64=uid token=token %}">{{ protocol }}://{{ domain }}{% url 'accounts:delete' uidb64=uid token=token %}</a></p>

<br>

<p><strong>Important Information:</strong></p>
<ul>
<li>Once confirmed, all your personal data will be permanently deleted or anonymized, except for competitions and submissions retained under our user agreement.</li>
<li>After deletion, you will no longer be eligible for any cash prizes in ongoing or future competitions.</li>
<li>If you wish to delete any submissions, please do so before confirming your account deletion.</li>
</ul>

<br>

<p>If you did not request this action or have changed your mind, you can safely ignore this email, and your account will remain intact.</p>

<br>

<p>Thank you for being part of our community.</p>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends 'emails/base_email.html' %}

{% block content %}
<p>Your account has been successfully removed. Thank you for being part of our community.</p>
<br>
<p>If you change your mind, you can create a new account at any time. We'd be happy to welcome you back!</p>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ <h2>User account deletion notice</h2>

<p>The following user has removed their account:</p>
<ul>
<li><strong>id:</strong> {{ user.id }}</li>
<li><strong>username:</strong> {{ user.username }}</li>
<li><strong>email:</strong> {{ user.email }}</li>
<li><strong>id:</strong> {{ deleted_user.id }}</li>
<li><strong>username:</strong> {{ deleted_user.username }}</li>
<li><strong>email:</strong> {{ deleted_user.email }}</li>
</ul>

<br>
Expand Down
118 changes: 118 additions & 0 deletions src/templates/profiles/emails/template_delete_account_notice.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{% extends 'emails/base_email.html' %}

{% block content %}
<h2>User account deletion notice</h2>

<p>You are receiving this notice because your are an administrator of the platform.</p>

<br>

<p>The following user has removed their account:</p>
<ul>
<li><strong>id:</strong> {{ deleted_user.id }}</li>
<li><strong>username:</strong> {{ deleted_user.username }}</li>
<li><strong>email:</strong> {{ deleted_user.email }}</li>
</ul>

<br>

<h3>Summary</h3>

<ul>
<li>Organizations: {{ organizations|length }}</li>
<li>Competitions owner: {{ competitions_organizer|length }} </li>
<li>Competitions participation: {{ competitions_participation|length }}</li>
<li>Submissions: {{ submissions|length }}</li>
<li>Data: {{ data|length }}</li>
<li>DataGroups: {{ data_groups|length }}</li>
<li>Tasks: {{ tasks|length }}</li>
<li>Queues: {{ queues|length }}</li>
<li>Posts: {{ posts|length }}</li>
</ul>

<h3>Details</h3>

<h4>Organizations the user is part of:</h4>
<ul>
{% for organization in organizations.all %}
<li>
<a class="item" href="{% url 'profiles:organization_profile' pk=organization.id %}">
{{ organization }}
</a>
</li>
{% endfor %}
</ul>

<h4>Competitions the user is the owner:</h4>
<ul>
{% for competition in competitions_organizer.all %}
<li>
<a class="item" href="{% url 'competitions:detail' pk=competition.pk %}">
{{ competition }}
</a>
</li>
{% endfor %}
</ul>

<h4>Competitions the user participated in:</h4>
<ul>
{% for competition in competitions_participation.all %}
<li>
<a class="item" href="{% url 'competitions:detail' pk=competition.pk %}">
{{ competition }}
</a>
</li>
{% endfor %}
</ul>

<h4>Submissions from the user:</h4>
<ul>
{% for submission in submissions.all %}
<li>
{{ submission }}
</li>
{% endfor %}
</ul>

<h4>Data created by the user</h4>
<ul>
{% for d in data.all %}
<li>
{{ d }}
</li>
{% endfor %}
</ul>

<h4>DataGroups created by the user</h4>
<ul>
{% for data_group in data_groups.all %}
<li>{{ data_group }}</li>
{% endfor %}
</ul>

<h4>Tasks created by the user</h4>
<ul>
{% for task in tasks.all %}
<li>
<a class="item" href="{% url 'tasks:detail' pk=task.pk %}">
{{ task }}
</a>
</li>
{% endfor %}
</ul>

<h4>Queues created by the user</h4>
<ul>
{% for queue in queues.all %}
<li>{{ queue }}</li>
{% endfor %}
</ul>

<h4>Posts posted by the user</h4>
<ul>
{% for post in posts.all %}
<li>{{ post }}</li>
{% endfor %}
</ul>

{% endblock %}