Reputation: 2230
I am trying the tutorial from DRF and I found something confused. I have a User model which simply extends auth.User like this
class User(DefaultUser):
"""
Represents a registered User
"""
EMAIL_VALIDATOR_LENGTH = 6
email_validated = models.BooleanField(default=False)
# using a 6-digit numbers for email validation
email_validator = models.CharField(
max_length=6,
default=_get_random_email_validator(EMAIL_VALIDATOR_LENGTH),
editable=False
)
phone_number = models.CharField(validators=[phone_regex],
blank=True, null=True, max_length=64)
# country is required
country = models.ForeignKey('Country', null=False, blank=False)
# subdivision is optional
subdivision = models.ForeignKey('Subdivision', null=True, blank=True)
Then I have my basic UserSerializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'email', 'password', 'email_validated',
'email_validator', 'country', 'subdivision', 'phone_number',
'last_login', 'is_superuser', 'username', 'first_name',
'last_name', 'is_staff', 'is_active', 'date_joined')
In my views.py, I have UserViewSet:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
@detail_route(methods=['get', 'post'], url_path='validate-email')
def validate_email(self, request, pk):
user = self.get_object()
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
user.is_active = True
user.save()
return Response({'status': 'email validated'})
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@detail_route(methods=['post'], url_path='set-password')
def set_password(self, request, pk):
pass
@list_route()
def test_list_route(self, request):
pass
The issue is, in validate_email, I actually only need pk but when I test the API, it told me that username and email are also required.
I then added following code to my UserSerializer
extra_kwargs = {'country': {'required': False},
'password': {'required': False},
'username': {'required': False},
}
Now the above issue is gone, but when I tried to create an User, I actually do want to require username and email.
Is there a way that I can specify which fields are required per action? For example, for my set_password(), I want to require the password field.
Thanks,
Upvotes: 0
Views: 2404
Reputation: 2230
I turned out to implement it myself, so I basically make all fields option but in actions, I added a decorator to ensure the request body has those specified keys in it.
decorator:
class AssertInRequest(object):
"""
A decorator to decorate ViewSet actions, this decorator assumes the first
positional argument is request, so you can apply this decorator any methods
that the first positional argument is request.
This decorator itself takes a list of strings as argument, and will check
that the request.data.dict() actually contains these keys
For example, if you have a action to set user password which you expect that
in the request body should have 'password' provided, use this decorator for
the method like
@detail_route()
@AssertInRequest(['password'])
def set_password(self, request, pk):
pass
"""
def __init__(self, keys):
self.keys = []
for key in keys:
if hasattr(key, 'upper'):
self.keys.append(key.lower())
def __call__(self, func):
def wrapped(*args, **kwargs):
if self.keys:
try:
request = args[1]
except IndexError:
request = kwargs['request']
if request:
json_data = get_json_data(request)
for key in self.keys:
if key not in json_data or not json_data[key]:
return DefaultResponse(
'Invalid request body, missing required data [%s]' % key,
status.HTTP_400_BAD_REQUEST)
return func(*args, **kwargs)
return wrapped
How to use it:
@detail_route(methods=['post'], url_path='set-password', permission_classes=(IsAuthenticated,))
@AssertInRequest(['password'])
def set_password(self, request, pk):
user = self.get_object()
json_data = get_json_data(request)
user.set_password(json_data['password'])
user.save()
return DefaultResponse(_('Successfully set password for user %s'
% user.email), status.HTTP_200_OK)
I guess it is not elegant but probably enough for me for now.
Upvotes: 0
Reputation: 330
Try to override serializer constructor to modify fields based on extra arguments. Didn't test that but it should work:
class UserSerializer(ModelSerializer):
def __init__(self, *args, **kwargs):
super(UserSerializer, self).__init__(*args, **kwargs)
require_password = kwargs.get('require_password', False)
require_email = kwargs.get('require_email', False)
if require_password:
self.fields['password'].required = True
if require_email:
self.fields['email'].required = True
Then pass require_password
or/and require_email
arguments when you need:
serializer = UserSerializer(data=request.data, require_password=True, require_email=True)
Upvotes: 0