Skip to content

Commit cc8a467

Browse files
Merge pull request #37 from RandomProgramm3r/develop
feat: Add User Profile endpoint with GET and PATCH support - Implemented `UserProfileSerializer` to handle partial updates: - Omits `avatar_url` from output when it’s empty or null - Hashes new passwords with `set_password()` without incrementing `token_version` (tokens remain valid) - Created `UserProfileView` (RetrieveUpdateAPIView) with `IsAuthenticated` permission - `GET /user/profile` returns current user’s data - `PATCH /user/profile` applies only provided fields - Registered route `path('user/profile', UserProfileView.as_view())`
2 parents 2dd9ae0 + aa55b6d commit cc8a467

File tree

3 files changed

+109
-8
lines changed

3 files changed

+109
-8
lines changed

promo_code/user/serializers.py

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import rest_framework_simplejwt.tokens
1111

1212
import user.constants
13-
import user.models as user_models
13+
import user.models
1414
import user.validators
1515

1616

@@ -27,10 +27,10 @@ class OtherFieldSerializer(rest_framework.serializers.Serializer):
2727
)
2828

2929
def validate(self, value):
30-
country = value['country'].upper()
30+
country = value['country']
3131

3232
try:
33-
pycountry.countries.lookup(country)
33+
pycountry.countries.lookup(country.upper())
3434
except LookupError:
3535
raise rest_framework.serializers.ValidationError(
3636
'Invalid ISO 3166-1 alpha-2 country code.',
@@ -79,7 +79,7 @@ class SignUpSerializer(rest_framework.serializers.ModelSerializer):
7979
other = OtherFieldSerializer(required=True)
8080

8181
class Meta:
82-
model = user_models.User
82+
model = user.models.User
8383
fields = (
8484
'name',
8585
'surname',
@@ -91,17 +91,17 @@ class Meta:
9191

9292
def create(self, validated_data):
9393
try:
94-
user = user_models.User.objects.create_user(
94+
user_ = user.models.User.objects.create_user(
9595
email=validated_data['email'],
9696
name=validated_data['name'],
9797
surname=validated_data['surname'],
9898
avatar_url=validated_data.get('avatar_url'),
9999
other=validated_data['other'],
100100
password=validated_data['password'],
101101
)
102-
user.token_version += 1
103-
user.save()
104-
return user
102+
user_.token_version += 1
103+
user_.save()
104+
return user_
105105
except django.core.exceptions.ValidationError as e:
106106
raise rest_framework.serializers.ValidationError(e.messages)
107107

@@ -168,3 +168,79 @@ def get_token(cls, user):
168168
token = super().get_token(user)
169169
token['token_version'] = user.token_version
170170
return token
171+
172+
173+
class UserProfileSerializer(rest_framework.serializers.ModelSerializer):
174+
name = rest_framework.serializers.CharField(
175+
required=False,
176+
min_length=user.constants.NAME_MIN_LENGTH,
177+
max_length=user.constants.NAME_MAX_LENGTH,
178+
)
179+
surname = rest_framework.serializers.CharField(
180+
required=False,
181+
min_length=user.constants.SURNAME_MIN_LENGTH,
182+
max_length=user.constants.SURNAME_MAX_LENGTH,
183+
)
184+
email = rest_framework.serializers.EmailField(
185+
required=False,
186+
min_length=user.constants.EMAIL_MIN_LENGTH,
187+
max_length=user.constants.EMAIL_MAX_LENGTH,
188+
validators=[
189+
user.validators.UniqueEmailValidator(
190+
'This email address is already registered.',
191+
'email_conflict',
192+
),
193+
],
194+
)
195+
password = rest_framework.serializers.CharField(
196+
write_only=True,
197+
required=False,
198+
validators=[django.contrib.auth.password_validation.validate_password],
199+
max_length=user.constants.PASSWORD_MAX_LENGTH,
200+
min_length=user.constants.PASSWORD_MIN_LENGTH,
201+
style={'input_type': 'password'},
202+
)
203+
avatar_url = rest_framework.serializers.CharField(
204+
required=False,
205+
max_length=user.constants.AVATAR_URL_MAX_LENGTH,
206+
validators=[
207+
django.core.validators.URLValidator(schemes=['http', 'https']),
208+
],
209+
)
210+
other = OtherFieldSerializer(required=False)
211+
212+
class Meta:
213+
model = user.models.User
214+
fields = (
215+
'name',
216+
'surname',
217+
'email',
218+
'password',
219+
'avatar_url',
220+
'other',
221+
)
222+
223+
def update(self, instance, validated_data):
224+
password = validated_data.pop('password', None)
225+
226+
if password:
227+
# do not invalidate the token
228+
instance.set_password(password)
229+
230+
other_data = validated_data.pop('other', None)
231+
if other_data is not None:
232+
instance.other = other_data
233+
234+
for attr, value in validated_data.items():
235+
setattr(instance, attr, value)
236+
237+
instance.save()
238+
return instance
239+
240+
def to_representation(self, instance):
241+
data = super().to_representation(instance)
242+
# If the response structure implies that a field is optional,
243+
# the server MUST NOT return the field if it is missing.
244+
if not instance.avatar_url:
245+
data.pop('avatar_url', None)
246+
return data

promo_code/user/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@
2222
rest_framework_simplejwt.views.TokenRefreshView.as_view(),
2323
name='user-token-refresh',
2424
),
25+
django.urls.path(
26+
'profile',
27+
user.views.UserProfileView.as_view(),
28+
name='user-profile',
29+
),
2530
]

promo_code/user/views.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import rest_framework.generics
2+
import rest_framework.permissions
23
import rest_framework.response
34
import rest_framework.status
45
import rest_framework_simplejwt.tokens
56
import rest_framework_simplejwt.views
67

8+
import user.models
79
import user.serializers
810

911

@@ -37,3 +39,21 @@ class UserSignInView(
3739
rest_framework_simplejwt.views.TokenObtainPairView,
3840
):
3941
serializer_class = user.serializers.SignInSerializer
42+
43+
44+
class UserProfileView(
45+
rest_framework.generics.RetrieveUpdateAPIView,
46+
):
47+
"""
48+
Retrieve (GET) and partially update (PATCH)
49+
detailed user profile information.
50+
"""
51+
http_method_names = ['get', 'patch', 'options', 'head']
52+
serializer_class = user.serializers.UserProfileSerializer
53+
permission_classes = [rest_framework.permissions.IsAuthenticated]
54+
55+
def get_object(self):
56+
return self.request.user
57+
58+
def patch(self, request, *args, **kwargs):
59+
return self.partial_update(request, *args, **kwargs)

0 commit comments

Comments
 (0)