简体   繁体   中英

django rest framework how to define required fields per ViewSet action

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,

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)

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM