From f1033a2313c75fe5570ff9d9be52b66d7d0c7fa1 Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Sat, 15 Feb 2025 04:45:21 +0000 Subject: [PATCH 1/8] Remove usage of username in favor of email --- temba/api/support.py | 4 +-- temba/orgs/views/tests/test_loginviews.py | 6 ++-- temba/orgs/views/tests/test_orgcrudl.py | 2 -- temba/orgs/views/tests/test_usercrudl.py | 1 - temba/orgs/views/views.py | 2 +- temba/tests/base.py | 2 +- temba/tests/crudl.py | 2 +- ...11_alter_user_email_alter_user_username.py | 23 +++++++++++++ temba/users/models.py | 32 +++++++------------ templates/orgs/email/user_forget.html | 2 +- templates/orgs/email/user_forget.txt | 2 +- templates/orgs/user_edit.html | 2 +- templates/staff/user_delete.html | 4 +-- 13 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 temba/users/migrations/0011_alter_user_email_alter_user_username.py diff --git a/temba/api/support.py b/temba/api/support.py index 0c927ecd3b3..84037f45922 100644 --- a/temba/api/support.py +++ b/temba/api/support.py @@ -66,7 +66,7 @@ class APIBasicAuthentication(RequestAttributesMixin, BasicAuthentication): Clients should authenticate using HTTP Basic Authentication. - Credentials: username:api_token + Credentials: email:api_token """ def authenticate_credentials(self, userid, password, request=None): @@ -75,7 +75,7 @@ def authenticate_credentials(self, userid, password, request=None): except APIToken.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid token or email") - if token.user.username != userid: + if token.user.email != userid: raise exceptions.AuthenticationFailed("Invalid token or email") if token.user.is_active: diff --git a/temba/orgs/views/tests/test_loginviews.py b/temba/orgs/views/tests/test_loginviews.py index 240f5fb850c..2a0245be1d7 100644 --- a/temba/orgs/views/tests/test_loginviews.py +++ b/temba/orgs/views/tests/test_loginviews.py @@ -38,7 +38,7 @@ def test_login(self): self.assertFormError( response.context["form"], None, - "Please enter a correct username and password. Note that both fields may be case-sensitive.", + "Please enter a correct email address and password. Note that both fields may be case-sensitive.", ) # submit incorrect password by case sensitivity @@ -47,7 +47,7 @@ def test_login(self): self.assertFormError( response.context["form"], None, - "Please enter a correct username and password. Note that both fields may be case-sensitive.", + "Please enter a correct email address and password. Note that both fields may be case-sensitive.", ) # submit correct username and password @@ -131,7 +131,7 @@ def test_login_lockouts(self): self.assertFormError( response.context["form"], None, - "Please enter a correct username and password. Note that both fields may be case-sensitive.", + "Please enter a correct email address and password. Note that both fields may be case-sensitive.", ) # and successful logins diff --git a/temba/orgs/views/tests/test_orgcrudl.py b/temba/orgs/views/tests/test_orgcrudl.py index 141904023e0..c7febefab66 100644 --- a/temba/orgs/views/tests/test_orgcrudl.py +++ b/temba/orgs/views/tests/test_orgcrudl.py @@ -828,9 +828,7 @@ def test_signup(self): response = self.client.post(edit_url, post_data, HTTP_X_FORMAX=True) self.assertEqual(200, response.status_code) - self.assertTrue(User.objects.get(username="myal@wr.org")) self.assertTrue(User.objects.get(email="myal@wr.org")) - self.assertFalse(User.objects.filter(username="myal@relieves.org")) self.assertFalse(User.objects.filter(email="myal@relieves.org")) def test_create_new(self): diff --git a/temba/orgs/views/tests/test_usercrudl.py b/temba/orgs/views/tests/test_usercrudl.py index 58e1f3fef81..a4008993f67 100644 --- a/temba/orgs/views/tests/test_usercrudl.py +++ b/temba/orgs/views/tests/test_usercrudl.py @@ -323,7 +323,6 @@ def test_edit(self): ) self.admin.refresh_from_db() - self.assertEqual("admin@trileet.com", self.admin.username) self.assertEqual("admin@trileet.com", self.admin.email) self.assertEqual("U", self.admin.email_status) # because email changed self.assertNotEqual("old-email-secret", self.admin.email_verification_secret) diff --git a/temba/orgs/views/views.py b/temba/orgs/views/views.py index 1208055ced9..f0cf9f04bb0 100644 --- a/temba/orgs/views/views.py +++ b/temba/orgs/views/views.py @@ -759,7 +759,7 @@ def pre_save(self, obj): obj = super().pre_save(obj) # keep our username and email in sync and record if email is changing - obj.username = obj.email + # obj.username = obj.email # get existing email address to know if it's changing obj._prev_email = User.objects.get(id=obj.id).email diff --git a/temba/tests/base.py b/temba/tests/base.py index efe480fb741..64aaf848999 100644 --- a/temba/tests/base.py +++ b/temba/tests/base.py @@ -143,7 +143,7 @@ def tearDown(self): def login(self, user, *, update_last_auth_on: bool = True, choose_org=None): self.assertTrue( - self.client.login(username=user.username, password=self.default_password), + self.client.login(username=user.email, password=self.default_password), f"couldn't login as {user.username}:{self.default_password}", ) diff --git a/temba/tests/crudl.py b/temba/tests/crudl.py index 81194b8e25d..980a344b986 100644 --- a/temba/tests/crudl.py +++ b/temba/tests/crudl.py @@ -14,7 +14,7 @@ def requestView(self, url, user, *, post_data=None, checks=(), choose_org=None, """ method = "POST" if post_data is not None else "GET" - user_name = user.username if user else "anonymous" + user_name = user.email if user else "anonymous" msg_prefix = f"{method} {url} as {user_name}" pre_msg_prefix = f"before {msg_prefix}" diff --git a/temba/users/migrations/0011_alter_user_email_alter_user_username.py b/temba/users/migrations/0011_alter_user_email_alter_user_username.py new file mode 100644 index 00000000000..b1583495f84 --- /dev/null +++ b/temba/users/migrations/0011_alter_user_email_alter_user_username.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.4 on 2025-02-14 21:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0010_alter_user_managers_alter_user_date_joined_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="email", + field=models.EmailField(max_length=254, unique=True, verbose_name="email address"), + ), + migrations.AlterField( + model_name="user", + name="username", + field=models.CharField(max_length=150, null=True, verbose_name="username"), + ), + ] diff --git a/temba/users/models.py b/temba/users/models.py index 88f7e0c2c7b..4e6ca4cf699 100644 --- a/temba/users/models.py +++ b/temba/users/models.py @@ -2,7 +2,6 @@ from django.conf import settings from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager as AuthUserManager -from django.contrib.auth.validators import UnicodeUsernameValidator from django.core.files.storage import storages from django.db import models from django.utils import timezone @@ -68,22 +67,15 @@ class User(AbstractBaseUser, PermissionsMixin): ) EMAIL_FIELD = "email" - USERNAME_FIELD = "username" - REQUIRED_FIELDS = ["email"] - - username = models.CharField( - _("username"), - max_length=150, - unique=True, - help_text=_("Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."), - validators=[UnicodeUsernameValidator()], - error_messages={ - "unique": _("A user with that username already exists."), - }, - ) + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + # username is on its way out + username = models.CharField(_("username"), max_length=150, null=True) + first_name = models.CharField(_("first name"), max_length=150, blank=True) last_name = models.CharField(_("last name"), max_length=150, blank=True) - email = models.EmailField(_("email address"), blank=True) + email = models.EmailField(_("email address"), unique=True) language = models.CharField(max_length=8, choices=settings.LANGUAGES, default=settings.DEFAULT_LANGUAGE) avatar = models.ImageField(upload_to=UploadToIdPathAndRename("avatars/"), storage=storages["public"], null=True) @@ -157,9 +149,11 @@ def get_orgs_for_request(cls, request): @classmethod def get_system_user(cls): - user = cls.objects.filter(username=cls.SYSTEM_USER_USERNAME).first() + user = cls.objects.filter(email=cls.SYSTEM_USER_USERNAME).first() if not user: - user = cls.objects.create_user(cls.SYSTEM_USER_USERNAME, first_name="System", last_name="Update") + user = cls.objects.create_user( + cls.SYSTEM_USER_USERNAME, email=cls.SYSTEM_USER_USERNAME, first_name="System", last_name="Update" + ) return user @property @@ -265,11 +259,9 @@ def release(self, user): """ Releases this user, and any orgs of which they are the sole owner. """ - user_uuid = str(uuid4()) self.first_name = "" self.last_name = "" - self.email = f"{user_uuid}@rapidpro.io" - self.username = f"{user_uuid}@rapidpro.io" + self.email = f"{str(uuid4())}@rapidpro.io" self.password = "" self.is_active = False self.save() diff --git a/templates/orgs/email/user_forget.html b/templates/orgs/email/user_forget.html index de17d613003..aa1a08721be 100644 --- a/templates/orgs/email/user_forget.html +++ b/templates/orgs/email/user_forget.html @@ -5,7 +5,7 @@

{% trans "Hi there" %}

{% trans "Someone has requested that the password for this email address be reset." %}

- {% blocktrans trimmed with email=user.username %} + {% blocktrans trimmed with email=user.email %} Clicking on the following link will allow you to reset the password for the account {{ email }}: {% endblocktrans %}

diff --git a/templates/orgs/email/user_forget.txt b/templates/orgs/email/user_forget.txt index e861544b96b..8d32edb21a2 100644 --- a/templates/orgs/email/user_forget.txt +++ b/templates/orgs/email/user_forget.txt @@ -5,7 +5,7 @@ {% trans "Someone has requested that the password for this email address be reset." %} -{% blocktrans trimmed with email=user.username %} +{% blocktrans trimmed with email=user.email %} Clicking on the following link will allow you to reset the password for the account {{ email }}: {% endblocktrans %} diff --git a/templates/orgs/user_edit.html b/templates/orgs/user_edit.html index 2532b65c7b2..e0b311a08c0 100644 --- a/templates/orgs/user_edit.html +++ b/templates/orgs/user_edit.html @@ -29,7 +29,7 @@ {% block summary %}
- {% blocktrans trimmed with email=user.username %} + {% blocktrans trimmed with email=user.email %} Your email address is {{ email }} {% endblocktrans %}
diff --git a/templates/staff/user_delete.html b/templates/staff/user_delete.html index 1b43e75d139..a1336d800ce 100644 --- a/templates/staff/user_delete.html +++ b/templates/staff/user_delete.html @@ -10,7 +10,7 @@
{% endif %} - {% blocktrans trimmed with username=object.username %} - Are you sure you want to delete {{ username }}?" + {% blocktrans trimmed with email=object.email %} + Are you sure you want to delete {{ email }}?" {% endblocktrans %} {% endblock fields %} From d26fb278f28af1c61686d46d1afe2beff8c3e0da Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Sat, 15 Feb 2025 04:53:01 +0000 Subject: [PATCH 2/8] Remove username syncing --- temba/orgs/views/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/temba/orgs/views/views.py b/temba/orgs/views/views.py index f0cf9f04bb0..3cea94e271c 100644 --- a/temba/orgs/views/views.py +++ b/temba/orgs/views/views.py @@ -758,9 +758,6 @@ def derive_initial(self): def pre_save(self, obj): obj = super().pre_save(obj) - # keep our username and email in sync and record if email is changing - # obj.username = obj.email - # get existing email address to know if it's changing obj._prev_email = User.objects.get(id=obj.id).email From 6c58867e9587168a2d9a2ad26666426ead71653e Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Sat, 22 Feb 2025 21:13:00 +0000 Subject: [PATCH 3/8] Use email in tests instead of username --- temba/api/v2/tests/test_base.py | 14 ++++---- temba/channels/tests.py | 3 +- temba/flows/tests/test_flow.py | 2 +- temba/orgs/models.py | 2 +- temba/orgs/views/tests/test_orgcrudl.py | 8 ++--- temba/orgs/views/views.py | 20 ++++++------ temba/staff/tests.py | 2 -- temba/staff/views.py | 2 +- temba/tests/base.py | 5 ++- temba/users/apps.py | 2 +- ...11_alter_user_email_alter_user_username.py | 2 +- temba/users/models.py | 32 +++++++++++++++---- 12 files changed, 55 insertions(+), 39 deletions(-) diff --git a/temba/api/v2/tests/test_base.py b/temba/api/v2/tests/test_base.py index 9c5d620f346..b720909a8a3 100644 --- a/temba/api/v2/tests/test_base.py +++ b/temba/api/v2/tests/test_base.py @@ -100,7 +100,7 @@ def request_by_session(endpoint, user, post_data=None): response = request_by_token(fields_url, token2.key, {"name": "Field 2", "type": "text"}) self.assertEqual(201, response.status_code) - response = request_by_basic_auth(fields_url, self.admin.username, token1.key) + response = request_by_basic_auth(fields_url, self.admin.email, token1.key) self.assertEqual(200, response.status_code) # can GET using session auth for admins, editors and servicing staff @@ -129,7 +129,7 @@ def request_by_session(endpoint, user, post_data=None): self.assertResponseError(response, None, "Invalid token", status_code=403) # can't fetch endpoint with invalid token - response = request_by_basic_auth(contacts_url, self.admin.username, "1234567890") + response = request_by_basic_auth(contacts_url, self.admin.email, "1234567890") self.assertResponseError(response, None, "Invalid token or email", status_code=403) # can't fetch endpoint with invalid username @@ -141,7 +141,7 @@ def request_by_session(endpoint, user, post_data=None): self.assertEqual(200, response.status_code) self.assertEqual(str(self.org.id), response["X-Temba-Org"]) - response = request_by_basic_auth(contacts_url, self.editor.username, token2.key) + response = request_by_basic_auth(contacts_url, self.editor.email, token2.key) self.assertEqual(200, response.status_code) self.assertEqual(str(self.org.id), response["X-Temba-Org"]) @@ -153,7 +153,7 @@ def request_by_session(endpoint, user, post_data=None): self.assertEqual(response.status_code, 429) # same with basic auth - response = request_by_basic_auth(fields_url, self.admin.username, token1.key) + response = request_by_basic_auth(fields_url, self.admin.email, token1.key) self.assertEqual(response.status_code, 429) # or if another user in same org makes a request @@ -168,7 +168,7 @@ def request_by_session(endpoint, user, post_data=None): self.org.api_rates = {"v2": "15000/hour"} self.org.save(update_fields=("api_rates",)) - response = request_by_basic_auth(fields_url, self.admin.username, token1.key) + response = request_by_basic_auth(fields_url, self.admin.email, token1.key) self.assertEqual(response.status_code, 200) cache.set(f"throttle_v2_{self.org.id}", [time.time() for r in range(15000)]) @@ -181,7 +181,7 @@ def request_by_session(endpoint, user, post_data=None): self.org.add_user(self.admin, OrgRole.AGENT) self.assertEqual(request_by_token(campaigns_url, token1.key).status_code, 403) - self.assertEqual(request_by_basic_auth(campaigns_url, self.admin.username, token1.key).status_code, 403) + self.assertEqual(request_by_basic_auth(campaigns_url, self.admin.email, token1.key).status_code, 403) # and if user is inactive, disallow the request self.org.add_user(self.admin, OrgRole.ADMINISTRATOR) @@ -191,7 +191,7 @@ def request_by_session(endpoint, user, post_data=None): response = request_by_token(contacts_url, token1.key) self.assertResponseError(response, None, "Invalid token", status_code=403) - response = request_by_basic_auth(contacts_url, self.admin.username, token1.key) + response = request_by_basic_auth(contacts_url, self.admin.email, token1.key) self.assertResponseError(response, None, "Invalid token or email", status_code=403) @override_settings(SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_HTTPS", "https")) diff --git a/temba/channels/tests.py b/temba/channels/tests.py index 13f45c08349..ae51dc7c0ae 100644 --- a/temba/channels/tests.py +++ b/temba/channels/tests.py @@ -6,7 +6,6 @@ from unittest.mock import patch from urllib.parse import quote -from smartmin.tests import SmartminTest from django.conf import settings from django.contrib.auth.models import Group @@ -1113,7 +1112,7 @@ def test_delete(self): self.assertNotIn(self.ex_channel, flow.channel_dependencies.all()) -class SyncEventTest(SmartminTest): +class SyncEventTest(TembaTest): def setUp(self): self.user = self.create_user("tito") self.org = Org.objects.create( diff --git a/temba/flows/tests/test_flow.py b/temba/flows/tests/test_flow.py index 4c8f28765f5..c18d46e9fb5 100644 --- a/temba/flows/tests/test_flow.py +++ b/temba/flows/tests/test_flow.py @@ -132,7 +132,7 @@ def test_ensure_current_version(self): # check we migrate to current spec version self.assertEqual("13.6.1", flow.version_number) self.assertEqual(2, flow.revisions.count()) - self.assertEqual("system", flow.revisions.order_by("id").last().created_by.username) + self.assertEqual("system", flow.revisions.order_by("id").last().created_by.email) # saved on won't have been updated but modified on will self.assertEqual(old_saved_on, flow.saved_on) diff --git a/temba/orgs/models.py b/temba/orgs/models.py index 6f353a06073..d9cfcb95990 100644 --- a/temba/orgs/models.py +++ b/temba/orgs/models.py @@ -916,7 +916,7 @@ def create_sample_flows(self, api_url): user = self.get_admins().first() if user: # some some substitutions - samples = samples.replace("{{EMAIL}}", user.username).replace("{{API_URL}}", api_url) + samples = samples.replace("{{EMAIL}}", user.email).replace("{{API_URL}}", api_url) try: self.import_app(json.loads(samples), user) diff --git a/temba/orgs/views/tests/test_orgcrudl.py b/temba/orgs/views/tests/test_orgcrudl.py index c7febefab66..0bfdbdc87d4 100644 --- a/temba/orgs/views/tests/test_orgcrudl.py +++ b/temba/orgs/views/tests/test_orgcrudl.py @@ -451,8 +451,8 @@ def test_org_grant(self): self.assertEqual(org.date_format, Org.DATE_FORMAT_DAY_FIRST) # check user exists and is admin - self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(username="john@carmack.com"))) - self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(username="tito@textit.com"))) + self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(email="john@carmack.com"))) + self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(email="tito@textit.com"))) # try a new org with a user that already exists instead del post_data["password"] @@ -464,8 +464,8 @@ def test_org_grant(self): org = Org.objects.get(name="id Software") self.assertEqual(org.date_format, Org.DATE_FORMAT_DAY_FIRST) - self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(username="john@carmack.com"))) - self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(username="tito@textit.com"))) + self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(email="john@carmack.com"))) + self.assertEqual(OrgRole.ADMINISTRATOR, org.get_user_role(User.objects.get(email="tito@textit.com"))) # try a new org with US timezone post_data["name"] = "Bulls" diff --git a/temba/orgs/views/views.py b/temba/orgs/views/views.py index 6e9675f1dde..33425b1ef8c 100644 --- a/temba/orgs/views/views.py +++ b/temba/orgs/views/views.py @@ -151,7 +151,7 @@ def post(self, request, *args, **kwargs): if not username: return self.form_invalid(form) - user = User.objects.filter(username__iexact=username).first() + user = User.objects.filter(email__iexact=username).first() valid_password = False # this could be a valid login by a user @@ -252,10 +252,10 @@ def form_invalid(self, form): lockout_timeout = getattr(settings, "USER_LOCKOUT_TIMEOUT", 10) failed_login_limit = getattr(settings, "USER_FAILED_LOGIN_LIMIT", 5) - FailedLogin.objects.create(username=user.username) + FailedLogin.objects.create(username=user.email) bad_interval = timezone.now() - timedelta(minutes=lockout_timeout) - failures = FailedLogin.objects.filter(username__iexact=user.username) + failures = FailedLogin.objects.filter(username__iexact=user.email) # if the failures reset after a period of time, then limit our query to that interval if lockout_timeout > 0: @@ -280,7 +280,7 @@ def form_valid(self, form): self.reset_user() # cleanup any failed logins - FailedLogin.objects.filter(username__iexact=user.username).delete() + FailedLogin.objects.filter(username__iexact=user.email).delete() return HttpResponseRedirect(self.get_success_url()) @@ -368,7 +368,7 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def get_username(self, form): - return self.request.user.username + return self.request.user.email class UserCRUDL(SmartCRUDL): @@ -678,7 +678,7 @@ def post_save(self, obj): obj.recovery_tokens.all().delete() # delete any failed login records - FailedLogin.objects.filter(username__iexact=obj.username).delete() + FailedLogin.objects.filter(username__iexact=obj.email).delete() return obj @@ -907,7 +907,7 @@ def get_context_data(self, **kwargs): brand = self.request.branding["name"] user = self.request.user - secret_url = pyotp.TOTP(user.two_factor_secret).provisioning_uri(user.username, issuer_name=brand) + secret_url = pyotp.TOTP(user.two_factor_secret).provisioning_uri(user.email, issuer_name=brand) context["secret_url"] = secret_url return context @@ -1817,7 +1817,7 @@ def pre_process(self, request, *args, **kwargs): # if user exists and is logged in then they just need to accept user = User.get_by_email(self.invitation.email) - if user and self.invitation.email.lower() == request.user.username.lower(): + if user and self.invitation.email.lower() == request.user.email.lower(): return HttpResponseRedirect(reverse("orgs.org_join_accept", args=[secret])) logout(request) @@ -1857,7 +1857,7 @@ def save(self, obj): ) # log the user in - user = authenticate(username=user.username, password=self.form.cleaned_data["password"]) + user = authenticate(username=user.email, password=self.form.cleaned_data["password"]) login(self.request, user) self.invitation.accept(user) @@ -1887,7 +1887,7 @@ def pre_process(self, request, *args, **kwargs): # if user doesn't already exist or we're logged in as a different user, we shouldn't be here user = User.get_by_email(self.invitation.email) - if not user or self.invitation.email != request.user.username: + if not user or self.invitation.email != request.user.email: return HttpResponseRedirect(reverse("orgs.org_join", args=[self.kwargs["secret"]])) return None diff --git a/temba/staff/tests.py b/temba/staff/tests.py index ca2bb5a53fe..fab34ad33e7 100644 --- a/temba/staff/tests.py +++ b/temba/staff/tests.py @@ -284,7 +284,6 @@ def test_update(self): self.editor.refresh_from_db() self.assertEqual("eddy@textit.com", self.editor.email) - self.assertEqual("eddy@textit.com", self.editor.username) # should match email self.assertEqual("Edward", self.editor.first_name) self.assertEqual("", self.editor.last_name) self.assertEqual({granters, betas}, set(self.editor.groups.all())) @@ -305,7 +304,6 @@ def test_update(self): self.editor.refresh_from_db() self.assertEqual("eddy@textit.com", self.editor.email) - self.assertEqual("eddy@textit.com", self.editor.username) self.assertEqual("Edward", self.editor.first_name) self.assertEqual("", self.editor.last_name) self.assertEqual({granters}, set(self.editor.groups.all())) diff --git a/temba/staff/views.py b/temba/staff/views.py index 806f0331311..a8a2304e68d 100644 --- a/temba/staff/views.py +++ b/temba/staff/views.py @@ -303,7 +303,7 @@ class Meta: title = "Update User" def pre_save(self, obj): - obj.username = obj.email + obj.email = obj.email return obj def post_save(self, obj): diff --git a/temba/tests/base.py b/temba/tests/base.py index dc4522d76ff..b79ed797411 100644 --- a/temba/tests/base.py +++ b/temba/tests/base.py @@ -144,7 +144,7 @@ def tearDown(self): def login(self, user, *, update_last_auth_on: bool = True, choose_org=None): self.assertTrue( self.client.login(username=user.email, password=self.default_password), - f"couldn't login as {user.username}:{self.default_password}", + f"couldn't login as {user.email}:{self.default_password}", ) # infer our org if we weren't handed one @@ -194,8 +194,7 @@ def get_flow(self, filename, substitutions=None, name=None): return flow def create_user(self, email, group_names=(), **kwargs): - user = User.objects.create_user(username=email, email=email, **kwargs) - user.set_password(self.default_password) + user = User.objects.create_user(email=email, password=self.default_password, **kwargs) user.save() for group in group_names: diff --git a/temba/users/apps.py b/temba/users/apps.py index ab549f68e58..1abdd1a080d 100644 --- a/temba/users/apps.py +++ b/temba/users/apps.py @@ -19,4 +19,4 @@ def on_post_migrate(sender, **kwargs): try: User.get_system_user() except User.DoesNotExist: - User._create_system_user() + User.objects.create_system_user() diff --git a/temba/users/migrations/0011_alter_user_email_alter_user_username.py b/temba/users/migrations/0011_alter_user_email_alter_user_username.py index b1583495f84..4d3cf94befa 100644 --- a/temba/users/migrations/0011_alter_user_email_alter_user_username.py +++ b/temba/users/migrations/0011_alter_user_email_alter_user_username.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-02-14 21:47 +# Generated by Django 5.1.4 on 2025-02-21 01:09 from django.db import migrations, models diff --git a/temba/users/models.py b/temba/users/models.py index f2cc17bd9c8..c2d657de5a4 100644 --- a/temba/users/models.py +++ b/temba/users/models.py @@ -50,12 +50,32 @@ class UserManager(AuthUserManager): Overrides the default user manager to make username lookups case insensitive """ - def get_by_natural_key(self, username): - return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": username}) + def get_by_natural_key(self, email): + return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": email}) + + def create_user(self, email, password, **extra_fields): + """ + Create and save a user with the given email and password. + """ + if not email: + raise ValueError(_("The Email must be set")) + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save() + return user + + def create_system_user(self): + """ + Creates the system user + """ + user = self.model(email=User.SYSTEM["email"], is_system=True) + user.save() + return user class User(AbstractBaseUser, PermissionsMixin): - SYSTEM = {"username": "system", "first_name": "System"} + SYSTEM = {"email": "system", "first_name": "System"} STATUS_UNVERIFIED = "U" STATUS_VERIFIED = "V" @@ -137,7 +157,7 @@ def get_or_create(cls, email: str, first_name: str, last_name: str, password: st @classmethod def get_by_email(cls, email: str): - return cls.objects.filter(username__iexact=email).first() + return cls.objects.filter(email__iexact=email).first() @classmethod def get_orgs_for_request(cls, request): @@ -152,7 +172,7 @@ def get_system_user(cls): """ Gets the system user """ - return cls.objects.get(email=cls.SYSTEM["username"]) + return cls.objects.get(email=cls.SYSTEM["email"]) @property def name(self) -> str: @@ -276,7 +296,7 @@ def release(self, user): org.remove_user(self) def __str__(self): - return self.name or self.username + return self.name or self.email class Meta: verbose_name = _("user") From d4983f5703de9b5ca0ecf9cac526787a49121092 Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Sat, 22 Feb 2025 21:18:11 +0000 Subject: [PATCH 4/8] linting --- temba/channels/tests.py | 1 - temba/users/models.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/temba/channels/tests.py b/temba/channels/tests.py index ae51dc7c0ae..265499cef14 100644 --- a/temba/channels/tests.py +++ b/temba/channels/tests.py @@ -6,7 +6,6 @@ from unittest.mock import patch from urllib.parse import quote - from django.conf import settings from django.contrib.auth.models import Group from django.core import mail diff --git a/temba/users/models.py b/temba/users/models.py index c2d657de5a4..9e48e4e6b7f 100644 --- a/temba/users/models.py +++ b/temba/users/models.py @@ -69,7 +69,7 @@ def create_system_user(self): """ Creates the system user """ - user = self.model(email=User.SYSTEM["email"], is_system=True) + user = self.model(email=User.SYSTEM["email"], first_name=User.SYSTEM["first_name"], is_system=True) user.save() return user From 0b9bd7e5d1d6c8e7b2a1c77477fb7e07f4aec632 Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Sat, 22 Feb 2025 21:43:52 +0000 Subject: [PATCH 5/8] Remove unnecessary assertion --- temba/users/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/temba/users/models.py b/temba/users/models.py index 9e48e4e6b7f..2c2e8b9171b 100644 --- a/temba/users/models.py +++ b/temba/users/models.py @@ -57,8 +57,6 @@ def create_user(self, email, password, **extra_fields): """ Create and save a user with the given email and password. """ - if not email: - raise ValueError(_("The Email must be set")) email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.set_password(password) From a44bd6d17148ca04a5b2f1a7caa0f590b4a733a1 Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Mon, 24 Feb 2025 19:35:16 +0000 Subject: [PATCH 6/8] Remvoe staff pre_save for user --- temba/staff/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/temba/staff/views.py b/temba/staff/views.py index a8a2304e68d..f7e9b827827 100644 --- a/temba/staff/views.py +++ b/temba/staff/views.py @@ -302,10 +302,6 @@ class Meta: success_message = "User updated successfully." title = "Update User" - def pre_save(self, obj): - obj.email = obj.email - return obj - def post_save(self, obj): """ Make sure our groups are up-to-date From af496534d4d97cbbaed1c732c76a73d5a92c45bc Mon Sep 17 00:00:00 2001 From: Eric Newcomer Date: Mon, 24 Feb 2025 19:45:56 +0000 Subject: [PATCH 7/8] Remove domain for deleted emails --- temba/users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temba/users/models.py b/temba/users/models.py index 2c2e8b9171b..17a16ec6bda 100644 --- a/temba/users/models.py +++ b/temba/users/models.py @@ -277,7 +277,7 @@ def release(self, user): """ self.first_name = "" self.last_name = "" - self.email = f"{str(uuid4())}@rapidpro.io" + self.email = str(uuid4()) self.password = "" self.is_active = False self.save() From 34cdad6fbc901941d8d7ce35b7140131d7e2b221 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Mon, 24 Feb 2025 20:08:25 +0000 Subject: [PATCH 8/8] Fix mailroom_db command --- temba/users/models.py | 4 ++-- temba/utils/management/commands/mailroom_db.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/temba/users/models.py b/temba/users/models.py index 17a16ec6bda..21119788630 100644 --- a/temba/users/models.py +++ b/temba/users/models.py @@ -50,10 +50,10 @@ class UserManager(AuthUserManager): Overrides the default user manager to make username lookups case insensitive """ - def get_by_natural_key(self, email): + def get_by_natural_key(self, email: str): return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": email}) - def create_user(self, email, password, **extra_fields): + def create_user(self, email: str, password: str, **extra_fields): """ Create and save a user with the given email and password. """ diff --git a/temba/utils/management/commands/mailroom_db.py b/temba/utils/management/commands/mailroom_db.py index cc9ec55a4f5..bc097f76a2c 100644 --- a/temba/utils/management/commands/mailroom_db.py +++ b/temba/utils/management/commands/mailroom_db.py @@ -271,7 +271,7 @@ def create_users(self, spec, org): for u in spec["users"]: user = User.objects.create_user( - u["email"], u["email"], USER_PASSWORD, first_name=u["first_name"], last_name=u["last_name"] + u["email"], USER_PASSWORD, first_name=u["first_name"], last_name=u["last_name"] ) team = org.teams.get(name=u["team"]) if u.get("team") else None org.add_user(user, OrgRole.from_code(u["role"]), team=team)