Added validation for countries, added patch method for profile page, code improvements and small fixes

This commit is contained in:
ITQ
2024-03-02 12:32:01 +03:00
parent 720e6319fe
commit 84b245ecf2
8 changed files with 115 additions and 99 deletions
+13 -5
View File
@@ -1,4 +1,5 @@
from django.db.models import Q
from django.conf import settings
from rest_framework.exceptions import ValidationError
from rest_framework.generics import ListAPIView, RetrieveAPIView
from countries.models import Country
@@ -13,10 +14,17 @@ class CountryListView(ListAPIView):
regions = self.request.query_params.get("region")
if regions:
regions_list = regions.split(",")
query = Q()
for region in regions_list:
query |= Q(region=region)
queryset = queryset.filter(query)
invalid_regions = [
region
for region in regions_list
if region not in settings.REGIONS
]
if invalid_regions:
invalid_regions_str = ", ".join(invalid_regions)
error_message = f"Invalid region(s): {invalid_regions_str}"
raise ValidationError(error_message)
queryset = queryset.filter(region__in=regions_list)
return queryset
+2 -2
View File
@@ -95,6 +95,8 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
REGIONS = ["Europe", "Africa", "Americas", "Oceania", "Asia"]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
@@ -115,8 +117,6 @@ REST_FRAMEWORK = {
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"users.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
),
}
+14 -3
View File
@@ -1,16 +1,27 @@
import jwt
from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.authentication import (
BaseAuthentication,
)
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated
from rest_framework.permissions import IsAuthenticated
from users.models import Profile
class JWTAuthentication(BaseAuthentication):
def authenticate_header(self, request):
return "Provide a valid token in the 'Authorization' header"
def authenticate(self, request):
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
if not token:
if IsAuthenticated in getattr(
request.resolver_match.func.cls, "permission_classes", []
):
raise NotAuthenticated
return None
try:
@@ -18,7 +29,7 @@ class JWTAuthentication(BaseAuthentication):
token, settings.SECRET_KEY, algorithms=["HS256"]
)
user = Profile.objects.get(login=payload["login"])
user = Profile.objects.get(id=payload["id"])
except Profile.DoesNotExist:
error = "Invalid token"
raise AuthenticationFailed(error) from None
@@ -1,28 +0,0 @@
# Generated by Django 4.2.10 on 2024-02-29 14:37
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('login', models.CharField(max_length=30, validators=[django.core.validators.RegexValidator('[a-zA-Z0-9-]+')])),
('email', models.EmailField(max_length=50)),
('password', models.CharField(max_length=100, validators=[django.core.validators.MinLengthValidator(6), django.core.validators.MaxLengthValidator(100), django.core.validators.RegexValidator('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$')])),
('countryCode', models.CharField(max_length=2, validators=[django.core.validators.RegexValidator('[a-zA-Z]{2}')])),
('isPublic', models.BooleanField()),
('phone', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator('\\+[\\d]+')])),
('image', models.URLField(blank=True, null=True)),
],
),
]
+33 -7
View File
@@ -1,34 +1,33 @@
from django.core.validators import (
MaxLengthValidator,
MinLengthValidator,
RegexValidator,
)
from django.db import models
from users.validators import CountryCodeValidator
class Profile(models.Model):
login = models.CharField(
max_length=30,
validators=[RegexValidator(r"[a-zA-Z0-9-]+")],
validators=[RegexValidator(r"^[a-zA-Z0-9-]+$")],
)
email = models.EmailField(max_length=50)
password = models.CharField(
max_length=100,
validators=[
MinLengthValidator(6),
MaxLengthValidator(100),
RegexValidator(r"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).+$"),
RegexValidator(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$"),
],
)
# ruff: noqa: DJ001 N815
countryCode = models.CharField(
max_length=2,
validators=[RegexValidator(r"[a-zA-Z]{2}")],
validators=[RegexValidator(r"[a-zA-Z]{2}"), CountryCodeValidator()],
)
isPublic = models.BooleanField()
phone = models.CharField(
max_length=20,
validators=[RegexValidator(r"\+[\d]+")],
validators=[MaxLengthValidator(20), RegexValidator(r"\+[\d]+")],
blank=True,
null=True,
)
@@ -39,3 +38,30 @@ class Profile(models.Model):
def is_authenticated(self):
return True
@classmethod
def check_unique(cls, user_id, validated_data):
errors = {}
if (
cls.objects.filter(login=validated_data.get("login"))
.exclude(id=user_id)
.exists()
):
errors["login"] = {"User with this login already exists"}
if (
cls.objects.filter(email=validated_data.get("email"))
.exclude(id=user_id)
.exists()
):
errors["email"] = {"User with this email already exists"}
if (
cls.objects.filter(phone=validated_data.get("phone"))
.exclude(id=user_id)
.exists()
):
errors["phone"] = {"User with this phone already exists"}
return errors
+14 -3
View File
@@ -3,9 +3,7 @@ from rest_framework import serializers
from users.models import Profile
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = [
@@ -17,3 +15,16 @@ class UserSerializer(serializers.ModelSerializer):
"phone",
"image",
]
class UpdateProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = [
"login",
"email",
"countryCode",
"isPublic",
"phone",
"image",
]
+16
View File
@@ -0,0 +1,16 @@
from countries.models import Country
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
from django.utils.deconstruct import deconstructible
@deconstructible
class CountryCodeValidator(BaseValidator):
def __init__(self, message=None):
self.message = message or "There is no such country"
def __call__(self, value):
try:
Country.objects.get(alpha2=value)
except Country.DoesNotExist:
raise ValidationError(self.message) from None
+23 -51
View File
@@ -1,4 +1,3 @@
import re
from datetime import timedelta
import bcrypt
@@ -11,62 +10,19 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from users.models import Profile
from users.serializers import UserSerializer
MIN_PASSWORD_LEN = 6
MAX_PASSWORD_LEN = 100
from users.serializers import ProfileSerializer, UpdateProfileSerializer
class RegisterUserApiView(APIView):
def post(self, request):
serializer = UserSerializer(data=request.data)
serializer = ProfileSerializer(data=request.data)
if serializer.is_valid():
if (
Profile.objects.filter(
login=serializer.validated_data["login"]
).first()
is not None
):
return Response(
{"error": "User with this login already exists"},
status=status.HTTP_409_CONFLICT,
)
if (
Profile.objects.filter(
email=serializer.validated_data["email"]
).first()
is not None
):
return Response(
{"error": "User with this email already exists"},
status=status.HTTP_409_CONFLICT,
)
if (
Profile.objects.filter(
phone=serializer.validated_data["phone"]
).first()
is not None
):
return Response(
{"error": "User with this phone already exists"},
status=status.HTTP_409_CONFLICT,
)
errors = Profile.check_unique(None, serializer.validated_data)
if errors:
return Response(errors, status=status.HTTP_409_CONFLICT)
password = serializer.validated_data["password"]
password_pattern = re.compile(
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$"
)
if not (bool(re.match(password_pattern, password))):
error = {
"error": "Your password does not meet our requirements"
}
return Response(
error,
status=status.HTTP_400_BAD_REQUEST,
)
password_hash = bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
@@ -114,7 +70,7 @@ class SigninUserApiView(APIView):
token = jwt.encode(
{
"login": login,
"id": user.id,
"password": password,
"exp": timezone.now() + timedelta(hours=24),
},
@@ -130,7 +86,23 @@ class ProfileMeApiView(APIView):
def get(self, request):
user = request.user
profile_data = self._get_profile_data(user)
return Response(profile_data)
def patch(self, request):
user = request.user
serializer = UpdateProfileSerializer(
user, data=request.data, partial=True
)
if serializer.is_valid():
errors = Profile.check_unique(user.id, serializer.validated_data)
if errors:
return Response(errors, status=status.HTTP_409_CONFLICT)
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def _get_profile_data(self, user):
profile = {
"login": user.login,
"email": user.email,
@@ -143,4 +115,4 @@ class ProfileMeApiView(APIView):
if user.image is not None:
profile["image"] = user.image
return Response(profile)
return profile