Skip to content

Commit fc856dd

Browse files
authored
allow username change, avoid new collisions on case insensitive match (#2061)
* allow username change, avoid new collisions on case insensitive match * fix existing profile edit test and test this feature * fix tests implicitly getting username from object * now a required field on user profile edit form
1 parent 4e47f01 commit fc856dd

File tree

4 files changed

+41
-5
lines changed

4 files changed

+41
-5
lines changed

users/forms.py

+12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class UserProfileForm(ModelForm):
99
class Meta:
1010
model = User
1111
fields = [
12+
'username',
1213
'first_name',
1314
'last_name',
1415
'email',
@@ -22,6 +23,17 @@ class Meta:
2223
'email_privacy': forms.RadioSelect,
2324
}
2425

26+
def clean_username(self):
27+
try:
28+
user = User.objects.get_by_natural_key(self.cleaned_data.get('username'))
29+
except User.MultipleObjectsReturned:
30+
raise forms.ValidationError('A user with that username already exists.')
31+
except User.DoesNotExist:
32+
return self.cleaned_data.get('username')
33+
if user == self.instance:
34+
return self.cleaned_data.get('username')
35+
raise forms.ValidationError('A user with that username already exists.')
36+
2537
def clean_email(self):
2638
email = self.cleaned_data.get('email')
2739
user = User.objects.filter(email=email).exclude(pk=self.instance.pk)

users/models.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22

33
from django.conf import settings
4-
from django.contrib.auth.models import AbstractUser
4+
from django.contrib.auth.models import AbstractUser, UserManager
55
from django.urls import reverse
66
from django.db import models
77
from django.utils import timezone
@@ -15,6 +15,12 @@
1515
DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'markdown')
1616

1717

18+
class CustomUserManager(UserManager):
19+
def get_by_natural_key(self, username):
20+
case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD)
21+
return self.get(**{case_insensitive_username_field: username})
22+
23+
1824
class User(AbstractUser):
1925
bio = MarkupField(blank=True, default_markup_type=DEFAULT_MARKUP_TYPE, escape_html=True)
2026

@@ -38,7 +44,7 @@ class User(AbstractUser):
3844

3945
public_profile = models.BooleanField('Make my profile public', default=True)
4046

41-
objects = UserManager()
47+
objects = CustomUserManager()
4248

4349
def get_absolute_url(self):
4450
return reverse('users:user_detail', kwargs={'slug': self.username})

users/tests/test_forms.py

+17
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def test_unique_email(self):
125125
User.objects.create_user('test42', '[email protected]', 'testpass')
126126

127127
form = UserProfileForm({
128+
'username': 'stanne',
128129
'email': '[email protected]',
129130
'search_visibility': 0,
130131
'email_privacy': 0,
@@ -134,3 +135,19 @@ def test_unique_email(self):
134135
form.errors,
135136
{'email': ['Please use a unique email address.']}
136137
)
138+
139+
def test_case_insensitive_unique_username(self):
140+
User.objects.create_user('stanne', '[email protected]', 'testpass')
141+
User.objects.create_user('test42', '[email protected]', 'testpass')
142+
143+
form = UserProfileForm({
144+
'username': 'Test42',
145+
'email': '[email protected]',
146+
'search_visibility': 0,
147+
'email_privacy': 0,
148+
}, instance=User.objects.get(username='stanne'))
149+
self.assertFalse(form.is_valid())
150+
self.assertEqual(
151+
form.errors,
152+
{'username': ['A user with that username already exists.']}
153+
)

users/tests/test_views.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def test_membership_update(self):
8484
self.assertEqual(response.status_code, 302) # Requires login now
8585

8686
self.assertTrue(self.user2.has_membership)
87-
self.client.login(username=self.user2, password='password')
87+
self.client.login(username=self.user2.username, password='password')
8888
response = self.client.get(url)
8989
self.assertEqual(response.status_code, 200)
9090

@@ -105,7 +105,7 @@ def test_membership_update(self):
105105
def test_membership_update_404(self):
106106
url = reverse('users:user_membership_edit')
107107
self.assertFalse(self.user.has_membership)
108-
self.client.login(username=self.user, password='password')
108+
self.client.login(username=self.user.username, password='password')
109109
response = self.client.get(url)
110110
self.assertEqual(response.status_code, 404)
111111

@@ -114,7 +114,7 @@ def test_user_has_already_have_membership(self):
114114
# has membership.
115115
url = reverse('users:user_membership_create')
116116
self.assertTrue(self.user2.has_membership)
117-
self.client.login(username=self.user2, password='password')
117+
self.client.login(username=self.user2.username, password='password')
118118
response = self.client.get(url)
119119
self.assertRedirects(response, reverse('users:user_membership_edit'))
120120

@@ -139,6 +139,7 @@ def test_user_update_redirect(self):
139139

140140
# should return 200 if the user does want to see their user profile
141141
post_data = {
142+
'username': 'username',
142143
'search_visibility': 0,
143144
'email_privacy': 1,
144145
'public_profile': False,

0 commit comments

Comments
 (0)