4.7. ‫کنترل‌کننده‌های داده‬

One of the capabilities of Backendpy framework is the input data handlers, which includes default or user-defined validators and filters. With this feature, the main handlers of the requests and the handlers of their input data are separated, and in this case, instead of the raw data, the validated and filtered data are received inside the main handlers.

With the regular and reusable structure of Data Handlers, much of the need for duplicate coding as well as unrelated to the main logic within the code is eliminated and speeds up project implementation.

Data handlers are defined as classes that inherit from the Data class.

For example:

project/apps/hello/controllers/data_handlers.py
from backendpy.data_handler.data import Data
from backendpy.data_handler.fields import String
from backendpy.data_handler import validators as v
from backendpy.data_handler import filters as f
...

class UserCreationData(Data):
    group = String('group', required=True, processors=[v.NotNull()], field_type=TYPE_URL_VARS)
    first_name = String('first_name', processors=[v.Length(max=50)])
    last_name = String('last_name', processors=[v.Length(max=50)])
    email = String('email', processors=[v.EmailAddress()])
    username = String('username', processors=[v.Unique(model=Users)])
    password = String('password', processors=[v.PasswordStrength()])
    password_re = String('password_re')

each of the items in the example is described below.

After defining a data handler, we must assign it to a request. This allocation is done in the routes definition section with the data_handler parameter:

project/apps/hello/controllers/handlers.py
from backendpy.router import Routes
from .data_handlers import UserCreationData

routes = Routes()

@routes.post(r'^/users$', data_handler=UserCreationData)
async def user_creation(request):
    data = request.cleaned_data
    ...

To get the final validated and filtered data inside the request main handler, we use request.cleaned_data, which will be a dictionary of data with defined fields in our data handler class.

4.7.1. Data fields

As shown in the previous example, data fields are defined inside the data handler class. Each field can be an instance of Field class or other data classes inherited from this base class.

In the example, String field is used. Developers can also create and use their own custom data fields as needed.

The parameters of the base field class are as follows:

4.7.2. Data processors

Processors are classes for processing data field values that include validators and filters.

A list of processors is assigned to a data field via the processors parameter and will run in sequence as specified. Also in this list, validators and filters can be used with any combination.

4.7.2.1. Validators

Validators are responsible for reviewing and validating data, and a data is either passed over or, if there is a discrepancy, the defined error is returned.

Developers can create and use the various validators they need by inheriting from the base Validator class.

Ready-made validators are also provided in the framework that can be used. The following is a list of them:

4.7.2.1.1. Default validators

Example:

token_type = String('token_type', required=True, processors=[v.NotNull(), v.In(['basic', 'bearer'])])

Example:

image = String('image', processors=[v.NotNull(), v.RestrictedFile(extensions=('jpg', 'jpeg', 'png'), min_size=1, max_size=2048)])

In this example, if the data we receive is a list of images instead of an image file, and we want these processors to be applied to all of those images, we can nest the list of processors inside another list as follows:

images = String('images', processors=[list((v.NotNull(), v.RestrictedFile(extensions=('jpg', 'jpeg', 'png'), min_size=1, max_size=2048)))])

Example:

username = String('username', processors=[v.Unique(model=Users)])

In this example, the value sent to the "username" field is queried directly to the "username" column from the "Users" model and checked for its uniqueness, and returns an error if it is exists.

In the previous example, if the name of the model table field is "user_id" instead of "username", we should change it as follows:

username = String('username', processors=[v.Unique(model=Users, model_field_name='user_id')])

The previous example was for adding a new user with a unique username to the database; However, if our request is to edit a user, the previous example should change as follows to prevent the error from being displayed when the user's current username is resubmitted:

username = String('username', processors=[v.Unique(model=Users, model_field_name='user_id', except_self='id')])

Note that in this example the "id" column of the model is used to identify each row of data. It is also necessary to send a field named "id" with the value of the current row id of this user in the database in the submitted data in order to exclude this row when checking the uniqueness of the username.

4.7.2.2. Filters

Filters are responsible for modifying data as needed, and changes are made when data passes through it.

Developers can create and use the various filters they need by inheriting from the base Filter class.

Default filters are also can be used:

4.7.2.2.1. Default filters

Example:

from backendpy.data_handler import validators as v
from backendpy.data_handler import filters as f
...

image = String('image', processors=(v.NotNull(), f.DecodeBase64(), v.RestrictedFile(extensions=('jpg', 'jpeg', 'png'), min_size=1, max_size=2048), f.ModifyImage(format='JPEG')))

In this example, a combination of validators and filters is used. First it checks that the value is not null, then it applies a filter to the received data and decodes it from base64 format, then it checks the allowed extensions for the received file with validator, and if it passes, it converts the file to jpeg format with another filter.