Skip to content

Commit 8606b17

Browse files
Didayologithub-actions[bot]Ihsan Ullahihsaan-ullahbbearce
authored
Merge pull request #1715 from codalab/develop
* Update version.json for release 1.15.0 (#1712) * Merge develop into master (#1405) * organization oidc login added * unused test file removed * http client * some changes * oidc login and signup added * oidc flow completed * Prevent LimitOverrunError with large output lines If a submission writes a output line larger than the stream buffer size ( default 64k ) a LimitOverrunError will be raise. Rather than using readline(...) use readutil(....) and in the case of a overrun just return the current buffer, the rest of the line will be returned with the next read. Signed-off-by: Chris Harris <[email protected]> * terms and condition check added * one terms checkbox for all organization login buttons * removed sandbox property from iframe to allow links in the iframe * Detailed results title removed * Detailed results configuration (#1374) * competition model updated * competition settings to allow participant to make submission public/private * unwanted restriction removed * detailed results now shown in submission panel and in leaderboard * submission tests updated --------- Signed-off-by: Chris Harris <[email protected]> Co-authored-by: Ihsan Ullah <[email protected]> Co-authored-by: Chris Harris <[email protected]> * Revert "Merge develop into master (#1405)" This reverts commit a26fc36. * More general exception in views.py (#1512) * More general exception in views.py * Update views.py * Update version.json for release 1.15.0 --------- Signed-off-by: Chris Harris <[email protected]> Co-authored-by: Adrien Pavão <[email protected]> Co-authored-by: Ihsan Ullah <[email protected]> Co-authored-by: Ihsan Ullah <[email protected]> Co-authored-by: Benjamin Bearce <[email protected]> Co-authored-by: Chris Harris <[email protected]> Co-authored-by: Nicolas Homberg <[email protected]> Co-authored-by: GitHub Actions <[email protected]> * Feature/remove user/soft removal (#1691) (#1716) * add soft deletions attributes in profile class + override deletion method + update email and log in mechanism * add emails template + soft delete + account view + deletion confirmation view * move the notice emails in the delete method of user * filter deleted users out of the front page stat * disable the action buttons on the list of participants modal in the competition management view Co-authored-by: Tristan Mary <[email protected]> * Updated the filters to show the new "Is Deleted" (#1717) * Updated the filters to show the new "Is Deleted" * Flake8 fixes --------- Co-authored-by: Obada Haddad <[email protected]> * .gitingore update to ignore the home page counters file. Also removed the file from cache so that it's not tracked anymore * Fix/remove user/soft removal (#1724) * Set is_active to False when deleting a user * change the email sending method and populate the missing txt files * add a checkbox to show/hide deleted users in the list of participant modal * Fixed the wrong user being named in the greetings in the email sent to admins upon account deletion * Flake8 fixed * restricted the usage of deletion link more than one time, used a different deletion token to expire it after deletion, account deletion modal now disappears after clickin the delete my account button --------- Co-authored-by: OhMaley <[email protected]> Co-authored-by: Obada Haddad <[email protected]> Co-authored-by: Ihsan Ullah <[email protected]> * Fix URLs in user deletion email (#1729) * Fix URLs in user deletion email * Fix domain --------- Signed-off-by: Chris Harris <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ihsan Ullah <[email protected]> Co-authored-by: Ihsan Ullah <[email protected]> Co-authored-by: Benjamin Bearce <[email protected]> Co-authored-by: Chris Harris <[email protected]> Co-authored-by: Nicolas Homberg <[email protected]> Co-authored-by: GitHub Actions <[email protected]> Co-authored-by: Tristan Mary <[email protected]> Co-authored-by: Obada Haddad-Soussac <[email protected]> Co-authored-by: Obada Haddad <[email protected]>
2 parents 32f11d9 + 30503a3 commit 8606b17

26 files changed

+713
-26
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ server_config.yaml
3737
.DS_Store?
3838

3939
caddy_config/
40-
caddy_data/
40+
caddy_data/
41+
42+
home_page_counters.json

home_page_counters.json

-6
This file was deleted.

src/apps/analytics/tasks.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,7 @@ def update_home_page_counters():
561561
public_competitions = Competition.objects.filter(published=True).count()
562562

563563
# Count active users
564-
# TODO: do not count deleted users
565-
users = User.objects.all().count()
564+
users = User.objects.filter(is_deleted=False).count()
566565

567566
# Count all submissions
568567
submissions = Submission.objects.all().count()

src/apps/api/serializers/competitions.py

+2
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ class CompetitionParticipantSerializer(serializers.ModelSerializer):
478478
username = serializers.CharField(source='user.username')
479479
is_bot = serializers.BooleanField(source='user.is_bot')
480480
email = serializers.CharField(source='user.email')
481+
is_deleted = serializers.BooleanField(source='user.is_deleted')
481482

482483
class Meta:
483484
model = CompetitionParticipant
@@ -487,6 +488,7 @@ class Meta:
487488
'is_bot',
488489
'email',
489490
'status',
491+
'is_deleted',
490492
)
491493

492494

src/apps/api/urls.py

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363
path('delete_unused_submissions/', quota.delete_unused_submissions, name="delete_unused_submissions"),
6464
path('delete_failed_submissions/', quota.delete_failed_submissions, name="delete_failed_submissions"),
6565

66+
# User account
67+
path('delete_account/', profiles.delete_account, name="delete_account"),
68+
6669
# Analytics
6770
path('analytics/storage_usage_history/', analytics.storage_usage_history, name='storage_usage_history'),
6871
path('analytics/competitions_usage/', analytics.competitions_usage, name='competitions_usage'),

src/apps/api/views/competitions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ class CompetitionParticipantViewSet(ModelViewSet):
795795
queryset = CompetitionParticipant.objects.all()
796796
serializer_class = CompetitionParticipantSerializer
797797
filter_backends = (DjangoFilterBackend, SearchFilter)
798-
filter_fields = ('user__username', 'user__email', 'status', 'competition')
798+
filter_fields = ('user__username', 'user__email', 'status', 'competition', 'user__is_deleted')
799799
search_fields = ('user__username', 'user__email',)
800800

801801
def get_queryset(self):

src/apps/api/views/profiles.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.contrib.auth.decorators import login_required
55
from django.db.models import Q
66
from django.http import HttpResponse
7-
from rest_framework.decorators import action
7+
from rest_framework.decorators import action, api_view
88
from rest_framework.exceptions import ValidationError, PermissionDenied
99
from rest_framework.generics import GenericAPIView, RetrieveAPIView
1010
from rest_framework import permissions, mixins
@@ -19,6 +19,7 @@
1919
OrganizationSerializer, MembershipSerializer, SimpleOrganizationSerializer, DeleteMembershipSerializer
2020
from profiles.helpers import send_mail
2121
from profiles.models import Organization, Membership
22+
from profiles.views import send_delete_account_confirmation_mail
2223

2324
User = get_user_model()
2425

@@ -84,6 +85,27 @@ def _get_data(user):
8485
)
8586

8687

88+
@api_view(['DELETE'])
89+
def delete_account(request):
90+
# Check data
91+
user = request.user
92+
is_username_valid = user.username == request.data["username"]
93+
is_password_valid = user.check_password(request.data["password"])
94+
95+
if is_username_valid and is_password_valid:
96+
send_delete_account_confirmation_mail(request, user)
97+
98+
return Response({
99+
"success": True,
100+
"message": "A confirmation link has been sent to your email. Follow the instruction to finish the process"
101+
})
102+
else:
103+
return Response({
104+
"success": False,
105+
"error": "Wrong username or password"
106+
})
107+
108+
87109
class OrganizationViewSet(mixins.CreateModelMixin,
88110
mixins.UpdateModelMixin,
89111
GenericViewSet):

src/apps/competitions/emails.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33

44
def get_organizer_emails(competition):
5-
return [user.email for user in competition.all_organizers]
5+
return [user.email for user in competition.all_organizers if not user.is_deleted]
66

77

88
def send_participation_requested_emails(participant):
9+
if participant.user.is_deleted:
10+
return
11+
912
context = {
1013
'participant': participant
1114
}
@@ -29,6 +32,9 @@ def send_participation_requested_emails(participant):
2932

3033

3134
def send_participation_accepted_emails(participant):
35+
if participant.user.is_deleted:
36+
return
37+
3238
context = {
3339
'participant': participant
3440
}
@@ -50,6 +56,9 @@ def send_participation_accepted_emails(participant):
5056

5157

5258
def send_participation_denied_emails(participant):
59+
if participant.user.is_deleted:
60+
return
61+
5362
context = {
5463
'participant': participant
5564
}
@@ -72,6 +81,9 @@ def send_participation_denied_emails(participant):
7281

7382

7483
def send_direct_participant_email(participant, content):
84+
if participant.user.is_deleted:
85+
return
86+
7587
codalab_send_markdown_email(
7688
subject=f'A message from the admins of {participant.competition.title}',
7789
markdown_content=content,

src/apps/profiles/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class UserAdmin(admin.ModelAdmin):
88
change_form_template = "admin/auth/user/change_form.html"
99
change_list_template = "admin/auth/user/change_list.html"
1010
search_fields = ['username', 'email']
11-
list_filter = ['is_staff', 'is_superuser', 'deleted', 'is_bot']
11+
list_filter = ['is_staff', 'is_superuser', 'deleted', 'is_deleted', 'is_bot']
1212
list_display = ['username', 'email', 'is_staff', 'is_superuser']
1313

1414

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 2.2.28 on 2024-11-20 16:07
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('profiles', '0013_auto_20240304_0616'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='user',
15+
name='deleted_at',
16+
field=models.DateTimeField(blank=True, null=True),
17+
),
18+
migrations.AddField(
19+
model_name='user',
20+
name='is_deleted',
21+
field=models.BooleanField(default=False),
22+
),
23+
]

src/apps/profiles/models.py

+69
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ class User(ChaHubSaveMixin, AbstractBaseUser, PermissionsMixin):
9393
# Required for social auth and such to create users
9494
objects = ChaHubUserManager()
9595

96+
# Soft deletion
97+
is_deleted = models.BooleanField(default=False)
98+
deleted_at = models.DateTimeField(null=True, blank=True)
99+
96100
def save(self, *args, **kwargs):
97101
self.slug = slugify(self.username, allow_unicode=True)
98102
super().save(*args, **kwargs)
@@ -193,6 +197,71 @@ def get_used_storage_space(self):
193197

194198
return storage_used
195199

200+
def delete(self, *args, **kwargs):
201+
"""Soft delete the user and anonymize personal data."""
202+
from .views import send_user_deletion_notice_to_admin, send_user_deletion_confirmed
203+
204+
# Send a notice to admins
205+
send_user_deletion_notice_to_admin(self)
206+
207+
# Mark the user as deleted
208+
self.is_deleted = True
209+
self.deleted_at = now()
210+
self.is_active = False
211+
212+
# Anonymize or removed personal data
213+
user_email = self.email # keep track of the email for the end of the procedure
214+
215+
# Github related
216+
self.github_uid = None
217+
self.avatar_url = None
218+
self.url = None
219+
self.html_url = None
220+
self.name = None
221+
self.company = None
222+
self.bio = None
223+
if self.github_info:
224+
self.github_info.login = None
225+
self.github_info.avatar_url = None
226+
self.github_info.gravatar_id = None
227+
self.github_info.html_url = None
228+
self.github_info.name = None
229+
self.github_info.company = None
230+
self.github_info.bio = None
231+
self.github_info.location = None
232+
233+
# Any user attribute
234+
self.username = f"deleted_user_{self.id}"
235+
self.slug = f"deleted_slug_{self.id}"
236+
self.photo = None
237+
self.email = None
238+
self.display_name = None
239+
self.first_name = None
240+
self.last_name = None
241+
self.title = None
242+
self.location = None
243+
self.biography = None
244+
self.personal_url = None
245+
self.linkedin_url = None
246+
self.twitter_url = None
247+
self.github_url = None
248+
249+
# Queues
250+
self.rabbitmq_username = None
251+
self.rabbitmq_password = None
252+
253+
# Save the changes
254+
self.save()
255+
256+
# Send a confirmation email notice to the removed user
257+
send_user_deletion_confirmed(user_email)
258+
259+
def restore(self, *args, **kwargs):
260+
"""Restore a soft-deleted user. Note that personal data remains anonymized."""
261+
self.is_deleted = False
262+
self.deleted_at = None
263+
self.save()
264+
196265

197266
class GithubUserInfo(models.Model):
198267
# Required Info

src/apps/profiles/tokens.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,23 @@
44

55
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
66
def _make_hash_value(self, user, timestamp):
7-
return (six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active))
7+
return (
8+
six.text_type(user.pk) +
9+
six.text_type(timestamp) +
10+
six.text_type(user.is_active)
11+
)
812

913

1014
account_activation_token = AccountActivationTokenGenerator()
15+
16+
17+
class AccountDeletionTokenGenerator(PasswordResetTokenGenerator):
18+
def _make_hash_value(self, user, timestamp):
19+
return (
20+
six.text_type(user.pk) +
21+
six.text_type(timestamp) +
22+
six.text_type(user.is_deleted)
23+
)
24+
25+
26+
account_deletion_token = AccountDeletionTokenGenerator()

src/apps/profiles/urls_accounts.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@
1414
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
1515
path('reset/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
1616
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
17+
path('user/<slug:username>/account/', views.UserAccountView.as_view(), name="user_account"),
18+
path('delete/<uidb64>/<token>', views.delete, name='delete'),
1719
]

0 commit comments

Comments
 (0)