With the custom User model created and the additional code to enforce the username to be a valid email, the next step is to allow a new user to register with an API call. For this, I get started with Django Rest Framework. After the usual installation and inclusion of ‘rest_framework’ in INSTALLED_APPS in settings.py file, DRF is ready to be used.
The first step is to create a UserSerializer class based on the custom User model:
class UserSerializer(serializers.ModelSerializer): '''Serializer for User model''' password = serializers.CharField( style={'input_type': 'password'}, write_only=True ) # def validate_username(self, value): # '''Check that username is a valid email''' # try: # validate_email(value) # except: # raise serializers.ValidationError('Username must be a valid email') # else: # return value def create(self, validated_data): '''Create new user in db''' new_user = User( username=validated_data['username'] ) new_user.set_password(validated_data['password']) # User should be inactive until email is validated new_user.is_active = False new_user.save() return new_user class Meta: model = User fields = ['username', 'password', 'is_active'] read_only_fields = ['is_active', ] # extra_args = { # 'password': { # 'write_only': True # } # }
Though documentation on Serializers and ModelSerializers is fairly extensive, to be able to code it needs a bit of googling and experimenting. The simplest implementation is merely to define:
class UserSerializer(serializers.ModelSerializer): '''Serializer for User model''' class Meta: model = User fields = ['username', 'password', 'is_active']
Next is to use the CreateAPIView with DRF to create a new model instance – in this case to register a new user.
class RegisterUserView(CreateAPIView): '''Register a new user''' serializer_class = UserSerializer
To be able to access this, we need to register a URL for the same. Since several urls will be related to the User model, a separate urls.py file can be created in the user_auth app directory. This will have the url:
urlpatterns = [ path('register-user', RegisterUserView.as_view(), name='register-user'), ]
The urls.py file in user_auth app directory can be included in the main urls.py file as:
from user_auth.urls import urlpatterns as user_urlpatterns urlpatterns = [ path('admin/', admin.site.urls), path('user/', include(user_urlpatterns)), ]
Now this
URL can be accessed in the web-browser with the development server on the link
user/register-user/ and this will give a form for the user. This form will have
three fields – username, password and is_active. To begin with, the is_active
field is not for the user to enter, but to be handled by the backend. The
intention is to create a new user upon registration, but to set the is_active
to False. When an email is set to the user’s email, the user will confirm the
registration and only then will the is_active be set to True. Of course, the
email integration will come later.
But first, this means that the is_active field should not appear in the default form. For that, the read_only_fields attribute need to be specified in class Meta. The next change that needs to be made is that the password is something that should only be written and should not be returned in an API response. For that, we can use the attribute extra_args and specify attributes for any of the fields. This is done with:
extra_args = { 'password': { 'write_only': True } }
The problem however is that the password form field in the browser does not hide the password and it appears in plain text. It might be possible to add more parameters to the password field in the extra_args. But, in such a case, it might be best to redefine the password field as a serializer field:
password = serializers.CharField( style={'input_type': 'password'}, write_only=True )
Here, the style has been defined with the input_type being password. In such a case of redefinition, the parameters specified in extra_args gets overwritten and so it is best to define all parameters and attributes in this redefinition itself.
With this, the form for registering a user looks OK – the password is hidden and is_active is not shown to the user. To be able to handle the form submission, the CreateAPIView provides a post method handler:
def post(self, *args, **kwargs): '''Create new user from API request''' user = UserSerializer(data=self.request.POST) if user.is_valid(): try: user.save() except Exception as e: return Response( data=e, status=status.HTTP_400_BAD_REQUEST ) else: error_list = [user.errors[e][0].title() for e in user.errors] return Response( data=error_list[0], status=status.HTTP_400_BAD_REQUEST ) return Response(user.data, status=status.HTTP_201_CREATED)
The advantage of specifying the serializer_class is that the data in the form can be directly dumped into the serializer from the POST data. One can check whether the serializer data is valid and only then create the model. But here there is the catch. The validation that has been created at the User model does not directly translate to the UserSerializer class even though it is based of the User model. I suppose one would have to redefine the fields of the UserSerializer with validation parameters. But it would defeat the purpose of redefining all fields of a User. So this means that checking for validity of UserSerializer data will not provide errors such as the username not being a valid email.
To be able to check for validity of UserSerializer data, it is also possible to define a validate_username method:
def validate_username(self, value): '''Check that username is a valid email''' try: validate_email(value) except: raise serializers.ValidationError('Username must be a valid email') else: return value
Defining such a method seems worse that redefining the serializer class fields as now this is repeating code. So, I have commented it out for now as the model validation should be good enough. This implies that at this moment the is_valid() check is useless as the serializer does not have any validation. I have kept it for now in case I add other validation in UserSerializer.
To be able to create a new user while also ensuring that the password is hashed and not stored directly as raw password, the create method of UserSerializer needs to be overridden:
def create(self, validated_data): '''Create new user in db''' new_user = User( username=validated_data['username'] ) new_user.set_password(validated_data['password']) # User should be inactive until email is validated new_user.is_active = False new_user.save() return new_user
Here, the checking that the username has to be valid email is performed and it will raise a ValidationError if that fails. So, the create method either returns the successfully created user or raises a ValidationError. The view then returns a 201 response with the new user or the validation error as error message with 400 response.
This works well with manual testing. Next, to clean up code and write tests.
No comments:
Post a Comment