Skip to content

Commit 9355de7

Browse files
feat: use shared behavior class for updating email and username
Refactor the current username update flow to use a shared behavior class and base updater view and serializer. This enables supporting email updates separately via a different endpoint.
1 parent 451f10c commit 9355de7

File tree

3 files changed

+166
-36
lines changed

3 files changed

+166
-36
lines changed

eox_core/api/support/v1/serializers.py

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,28 @@ class WrittableEdxappRemoveUserSerializer(serializers.Serializer):
2828
is_support_user = serializers.BooleanField(default=True)
2929

3030

31-
class WrittableEdxappUsernameSerializer(serializers.Serializer):
31+
class WrittableEdxappUserSerializer(serializers.Serializer):
3232
"""
33-
Handles the serialization of the data required to update the username of an edxapp user.
33+
Base serializer for updating username or email of an edxapp user.
3434
"""
35-
new_username = serializers.CharField(max_length=USERNAME_MAX_LENGTH, write_only=True)
3635

37-
def validate(self, attrs):
36+
def validate_conflicts(self, attrs):
3837
"""
39-
When a username update is being made, then it checks that:
40-
- The new username is not already taken by other user.
41-
- The user is not staff or superuser.
42-
- The user has just one signup source.
38+
Validates that no conflicts exist for the provided username or email.
4339
"""
4440
username = attrs.get("new_username")
45-
conflicts = check_edxapp_account_conflicts(None, username)
41+
email = attrs.get("new_email")
42+
43+
conflicts = check_edxapp_account_conflicts(email, username)
4644
if conflicts:
47-
raise serializers.ValidationError({"detail": "An account already exists with the provided username."})
45+
raise serializers.ValidationError({"detail": "An account already exists with the provided username or email."})
4846

47+
return attrs
48+
49+
def validate_role_restrictions(self, attrs):
50+
"""
51+
Validates that the user is not staff or superuser and has just one signup source.
52+
"""
4953
if self.instance.is_staff or self.instance.is_superuser:
5054
raise serializers.ValidationError({"detail": "You can't update users with roles like staff or superuser."})
5155

@@ -54,18 +58,81 @@ def validate(self, attrs):
5458

5559
return attrs
5660

57-
def update(self, instance, validated_data):
61+
def validate_required_fields(self, required_fields):
5862
"""
59-
Method to update username of edxapp User.
63+
Validates that at least one field to update is provided.
6064
"""
61-
key = 'username'
62-
if validated_data:
63-
setattr(instance, key, validated_data['new_username'])
64-
instance.save()
65+
if not required_fields:
66+
raise serializers.ValidationError(
67+
{"detail": "At least one field to update must be provided."}
68+
)
69+
return required_fields
70+
71+
72+
class WrittableEdxappUsernameSerializer(WrittableEdxappUserSerializer):
73+
"""
74+
Handles the serialization of the data required to update the username of an edxapp user.
75+
"""
76+
77+
new_username = serializers.CharField(
78+
max_length=USERNAME_MAX_LENGTH,
79+
required=True,
80+
allow_blank=False,
81+
allow_null=False,
82+
)
83+
84+
def validate(self, attrs):
85+
"""
86+
Validates that the new username is provided and passes all checks.
87+
"""
88+
if not attrs.get("new_username"):
89+
raise serializers.ValidationError({"detail": "You must provide a new username."})
90+
91+
self.validate_conflicts(attrs)
92+
self.validate_role_restrictions(attrs)
93+
94+
return attrs
6595

96+
def update(self, instance, validated_data):
97+
"""
98+
Updates the username of the edxapp User.
99+
"""
100+
instance.username = validated_data["new_username"]
101+
instance.save()
66102
return instance
67103

68104

105+
class WrittableEdxappEmailSerializer(WrittableEdxappUserSerializer):
106+
"""
107+
Handles the serialization of the data required to update the email of an edxapp user.
108+
"""
109+
110+
new_email = serializers.EmailField(
111+
required=True,
112+
allow_blank=False,
113+
allow_null=False,
114+
)
115+
116+
def validate(self, attrs):
117+
"""
118+
Validates that the new email is provided and passes all checks.
119+
"""
120+
if not attrs.get("new_email"):
121+
raise serializers.ValidationError({"detail": "You must provide a new email."})
122+
123+
self.validate_conflicts(attrs)
124+
self.validate_role_restrictions(attrs)
125+
126+
return attrs
127+
128+
def update(self, instance, validated_data):
129+
"""
130+
Updates the email of the edxapp User.
131+
"""
132+
instance.email = validated_data["new_email"]
133+
instance.save()
134+
return instance
135+
69136
class OauthApplicationUserSerializer(serializers.Serializer):
70137
"""
71138
Oauth Application owner serializer.

eox_core/api/support/v1/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
urlpatterns = [ # pylint: disable=invalid-name
1010
re_path(r'^user/$', views.EdxappUser.as_view(), name='edxapp-user'),
1111
re_path(r'^user/replace-username/$', views.EdxappReplaceUsername.as_view(), name='edxapp-replace-username'),
12+
re_path(r'^user/replace-email/$', views.EdxappReplaceEmail.as_view(), name='edxapp-replace-email'),
1213
re_path(r'^oauth-application/$', views.OauthApplicationAPIView.as_view(), name='edxapp-oauth-application'),
1314
]

eox_core/api/support/v1/views.py

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
OauthApplicationSerializer,
2727
WrittableEdxappRemoveUserSerializer,
2828
WrittableEdxappUsernameSerializer,
29+
WrittableEdxappEmailSerializer,
2930
)
3031
from eox_core.api.v1.serializers import EdxappUserReadOnlySerializer
3132
from eox_core.api.v1.views import UserQueryMixin
@@ -95,42 +96,35 @@ def delete(self, request, *args, **kwargs): # pylint: disable=too-many-locals
9596
return Response(message, status=status)
9697

9798

98-
class EdxappReplaceUsername(UserQueryMixin, APIView):
99+
class EdxappUserUpdateBase(UserQueryMixin, APIView):
99100
"""
100-
Handles the replacement of the username.
101+
Base view for updating edxapp user attributes (username, email, etc.).
102+
Provides common functionality for user updates with forum synchronization.
101103
"""
102104

103105
authentication_classes = (BearerAuthentication, SessionAuthentication, JwtAuthentication)
104106
permission_classes = (EoxCoreSupportAPIPermission,)
105107
renderer_classes = (JSONRenderer, BrowsableAPIRenderer)
106108

107-
@audit_drf_api(action="Update an Edxapp user's Username.", method_name='eox_core_api_method')
108-
def patch(self, request, *args, **kwargs):
109+
def get_serializer_class(self):
109110
"""
110-
Allows to safely update an Edxapp user's Username along with the
111-
forum associated User.
112-
113-
For now users that have different signup sources cannot be updated.
114-
115-
For example:
116-
117-
**Requests**:
118-
PATCH <domain>/eox-core/support-api/v1/replace-username/
111+
Returns the serializer class to use.
112+
Must be overridden by subclasses.
113+
"""
114+
raise NotImplementedError("Subclasses must implement get_serializer_class()")
119115

120-
**Request body**
121-
{
122-
"new_username": "new username"
123-
}
116+
def patch(self, request, *args, **kwargs):
117+
"""
118+
Updates an edxapp user's attribute and synchronizes with the forum.
124119
125-
**Response values**
126-
User serialized.
120+
For users with different signup sources, updates are not allowed.
127121
"""
128122
query = self.get_user_query(request)
129123
user = get_edxapp_user(**query)
130124
data = request.data
131125

132126
with transaction.atomic():
133-
serializer = WrittableEdxappUsernameSerializer(user, data=data)
127+
serializer = self.get_serializer_class()(user, data=data)
134128
serializer.is_valid(raise_exception=True)
135129
serializer.save()
136130

@@ -149,6 +143,74 @@ def patch(self, request, *args, **kwargs):
149143
return Response(serialized_user.data)
150144

151145

146+
class EdxappReplaceUsername(EdxappUserUpdateBase):
147+
"""
148+
Handles the replacement of the username.
149+
"""
150+
151+
def get_serializer_class(self):
152+
"""
153+
Returns the serializer class to use.
154+
"""
155+
return WrittableEdxappUsernameSerializer
156+
157+
@audit_drf_api(action="Update an Edxapp user's Username.", method_name='eox_core_api_method')
158+
def patch(self, request, *args, **kwargs):
159+
"""
160+
Allows to safely update an Edxapp user's Username along with the
161+
forum associated User.
162+
163+
For now users that have different signup sources cannot be updated.
164+
165+
For example:
166+
167+
**Requests**:
168+
PATCH <domain>/eox-core/support-api/v1/replace-username/?username=old_username
169+
170+
**Request body**
171+
{
172+
"new_username": "new username"
173+
}
174+
175+
**Response values**
176+
User serialized.
177+
"""
178+
return super().patch(request, *args, **kwargs)
179+
180+
181+
class EdxappReplaceEmail(EdxappUserUpdateBase):
182+
"""
183+
Handles the replacement of the email.
184+
"""
185+
186+
def get_serializer_class(self):
187+
"""Returns the serializer class to use."""
188+
return WrittableEdxappEmailSerializer
189+
190+
@audit_drf_api(action="Update an Edxapp user's Email.", method_name='eox_core_api_method')
191+
def patch(self, request, *args, **kwargs):
192+
"""
193+
Allows to safely update an Edxapp user's Email along with the
194+
forum associated User.
195+
196+
For now users that have different signup sources cannot be updated.
197+
198+
For example:
199+
200+
**Requests**:
201+
PATCH <domain>/eox-core/support-api/v1/replace-email/?email=old_email
202+
203+
**Request body**
204+
{
205+
"new_email": "new email"
206+
}
207+
208+
**Response values**
209+
User serialized.
210+
"""
211+
return super().patch(request, *args, **kwargs)
212+
213+
152214
class OauthApplicationAPIView(UserQueryMixin, APIView):
153215
"""
154216
Handles requests related to the

0 commit comments

Comments
 (0)