📜 ⬆️ ⬇️

Problems in the library of Django forms on the example of the phone entry field

As you know, Django includes a library for generating and maintaining html forms. Some time ago another library of forms was bundled with Django, but then it was completely rewritten. Perhaps, then the developers have solved a lot of architectural problems. But when working with the current library there are some difficulties. That's what I want to talk about.

So, the task. Users love to leave their phones and other private information on sites. Moreover, they want to do this without thinking about how to enter it correctly: 8 (908) 1271669 or, say, 908 127 16 69. Website visitors like to see the right phones, preferably uniformly designed: (+7 495) 722- 16-25, +7 968 ​​127-31-32. It turns out that you need to validate and store the numbers in a normalized form, that is, without registration. In the field about which I will talk, you can enter more than one phone number. The storage format is defined as a sequence of 11 digits, separated by a space.

For further narration, I need to briefly outline the principle of how forms work. A form consists of the Form class and a set of fields included in the form (the Field class). When the form is first created, the initial dictionary is passed to the initial values ​​for the fields. If we are talking about a ModelForm, the initial dictionary is automatically created from the model instance instance that was passed during creation. The Form class provides an interface for generating the code for the html form itself. The process is managed by instances of the BoundField class, linking the fields and the data contained in the form. The HTML code itself is generated by widgets (Widget class). When the user submits the completed form, the data dictionary is transferred to the form constructor - the contents of the POST request. Now the form fields should check the user input and make sure that all the fields are correct. In the event of an error, the form is generated again, but not the initial dictionary, but the user input data dictionary is taken as the values ​​for the fields.

As you can see, the data inside the form has three routes: from application to user (through initial when first creating the form), from user to user (re-displaying erroneously entered data) and from user to application (if the entered data are correct). Well, the task seems simple. It is necessary to hook into the first and third route, formatting the phones for the user and normalizing for the application. Let's start with the last one.
')
To begin, we will make a blank for the future of the field. Obviously, it must be inherited from CharField.

class MultiplePhoneFormField(forms.CharField): #    ,      . phone_code = '' 

The documentation describes all the methods involved in processing the value during validation. to_python () is used to cast to the correct data type for the application. But our data type is a string, so we will not use this method. Next are the validate () and run_validators () methods. They are used to check the correctness of the entered value, but cannot change it, therefore they are not suitable either. Remains the clean () method of the field. In the base implementation, it calls the above methods in the correct order and returns the final value. So, here and post the code.

  def clean(self, phones): phones = super(MultiplePhoneFormField, self).clean(phones) cleaned_phones = [] for phone in phones.split(','): phone = re.sub(r'[\s +.()\-]', '', phone) if not phone: continue if not phone.isdigit(): raise ValidationError(u'   .') if len(phone) == 11: pass elif len(phone) == 10: phone = '7' + phone elif len(self.phone_code + phone) == 11: phone = self.phone_code + phone else: raise ValidationError(u'  .') cleaned_phones.append(phone) return ' '.join(cleaned_phones) 

I will not describe in detail exactly how the number is being validated, I think, and so everything is clear.

Now the route from application to user. The documentation has an example implementation of the MultiEmailField field, which returns a list of email addresses to the application. But how it lists this list to the user is not mentioned. Apparently, it is implied that this task falls on the shoulders of the application creating the form. There are no other examples either. But we are not proud, we can see it in the source code.

The BoundField class has an as_widget () method, which passes a field value to this widget that needs to be displayed by calling its value () method. It is in this method that it is determined whether the data source is data or initial. And here we are waiting for a big disappointment: if the data is taken from initial, then the field can not be integrated into the process and change the data. The value () method simply calls self.form.initial.get (self.name) and then, regardless of the data source, passes it to the prepare_value () method of the field. It turns out that all values ​​pass through the same pipeline, at the end of which the “correct” value should turn out.

Either I didn’t understand something, or Jung forms are really designed so that only the application itself can prepare the data for output in the form. In the initial dictionary at the time of form creation, there should already be data ready for insertion into html.

“But wait, but how does DatetimeField work, which calmly takes datetime as initial?” You say. So I thought how. It turned out that the value received from an unknown source is passed to the render () method of the DateTimeInput widget, which in turn passes it to its _format_value () method. And already this method, if it finds that the value is datetime, converts it to a string. Why it is impossible to make also in our case? Because the type of the value transmitted from the application and received when sending the form is the same. In both cases, it is a string.

Nevertheless, the solution is necessary and it is. If you look at the BoundField.value () method again, you will notice that the value received from the user is additionally passed to the bound_data () method. Consequently, in the prepare_value () method, where the value falls after, you can determine where it came from, if you first mark it. So do.

 class ValueFromDatadict(unicode): pass class MultiplePhoneFormField(forms.CharField): #    ,      . phone_code = '' def bound_data(self, data, initial): return ValueFromDatadict(data) def prepare_value(self, value): if not value or isinstance(value, ValueFromDatadict): return value return ', '.join(format_phones(value, code=self.phone_code)) 

Hooray! Now phones are formatted when they are displayed in the form for the first time, and do not change when edited data comes from the user. And this is how you can format phones.

 def format_phones(phones, code=None): for phone in filter(None, phones.split(' ')): if len(phone) != 11: #  . pass elif phone[0:4] == '8800': # 8 800 100-31-32 phone = u'8 800 %s-%s-%s' % (phone[4:7], phone[7:9], phone[9:11]) elif code and phone.startswith(code): # (+7 351) 722-16-25 # (+7 3512) 22-16-25 phone = phone[len(code):] phone = u'(+%s %s) %s-%s-%s' % (code[0], code[1:], phone[:-4], phone[-4:-2], phone[-2:]) else: # +7 968 127-31-32 phone = u'+%s %s %s-%s-%s' % (phone[0], phone[1:4], phone[4:7], phone[7:9], phone[9:11]) yield phone 

It remains only to indicate in the form constructor the code of the city to which the edited object is attached.

 class RestaurantForm(forms.ModelForm): phone = MultiplePhoneFormField(label=u'', required=False, help_text=u'     .') def __init__(self, *args, **kwargs): super(RestaurantForm, self).__init__(*args, **kwargs) if self.instance: self.fields['phone'].phone_code = self.instance.city.phone_code 

Well, for the output of phones on the site will fit this filter:

 @register.filter def format_phones_with_code(phones, code): return mark_safe(u', '.join([u'<nobr>%s</nobr>' % phone for phone in format_phones(phones, code)])) 

Of course, we can assume that the task was solved. But obviously not without crutches. The same can be said about the field implementations bundled with Django. For example, in the to_python () method of the same DateTimeField field there is a check that the value is already of type datetime. In this case, the to_python () method is called only for values ​​obtained from the data dictionary. The documentation for the forms about the contents of the data dictionary clearly states: "These will usually be strings, but they are not required." Apparently, this makes some sense for validating something other than user input from a post request. But such flexibility introduces uncertainty and makes validation a heuristic, not an algorithmic, task.

Source: https://habr.com/ru/post/158107/


All Articles