| 1 | = Proposal: Improvements for django.forms = |
| 2 | |
| 3 | I would like to propose several (quite independent) improvements for django.forms. Their common goal is: '''It should be simple to write powerful and flexible forms.''' |
| 4 | |
| 5 | These proposals have some common assumptions: |
| 6 | * They try to expand possibilities in definitions of forms and API for views and templates. They do not try to change methods as_table, as_ul and as_li of forms - I think in Django there are templates for writing of HTML code and these helper methods should be used only for simple forms. |
| 7 | * They assume that forms are used with generic views and generic templates. (But it does not mean that they could not be useful in other situations.) |
| 8 | * They try to improve core Django forms - it is decision of contributed and external applications to use or not to use new possibilities. |
| 9 | |
| 10 | My proposals are not only ideas - all forms in my code are subclasses of django.forms' subclasses which use most of these possible improvements. But these subclasses are not nice, they assume to much about internals of Django forms and they are sometimes broken by changes in Django. |
| 11 | |
| 12 | And sorry for my English. |
| 13 | |
| 14 | == Fieldsets == |
| 15 | |
| 16 | I would like to propose new attribute for inner meta class of forms - "fieldsets". It would be list of dictionaries with two possible items - "fields" (list of fields) and "legend" (but I also propose items "attrs" and "template" in another proposal). Example: |
| 17 | |
| 18 | {{{ |
| 19 | class UserForm(forms.ModelForm): |
| 20 | class Meta: |
| 21 | model = User |
| 22 | fieldsets = ( |
| 23 | {'fields': ('username', 'first_name', 'last_name', 'email')}, |
| 24 | {'fields': ('is_active', 'is_staff', 'is_superuser'), 'legend': _('Permissions')}, |
| 25 | ) |
| 26 | }}} |
| 27 | |
| 28 | Forms would have two new methods - has_fieldsets and fieldsets. The second one would iterate all fieldsets - each item would be a dictionary with items "fields" (list of bound fields) and "legend". |
| 29 | |
| 30 | Fieldsets are useful also for common (non-model) forms - so this attribute should be defined for them. But I think it would be nice to move also "fields" and "exclude" attribute to common forms - it is useful for reusing of existing forms. |
| 31 | |
| 32 | == Inlines == |
| 33 | |
| 34 | I would like to propose inlines - forms with inner formsets for editing of 1..n relations. They could be defined with attribute "fieldsets" or "inlines". The second one would be the special case of the first one, "inlines" would be appended to "fieldsets". Example: |
| 35 | |
| 36 | {{{ |
| 37 | class UserForm(forms.ModelForm): |
| 38 | class Meta: |
| 39 | model = User |
| 40 | fieldsets = ( |
| 41 | {'fields': ('username', 'first_name', 'last_name', 'email')}, |
| 42 | forms.inlineformset_factory(User, MyModelWithLinkToUser), |
| 43 | {'fields': ('is_active', 'is_staff', 'is_superuser'), 'legend': _('Permissions')}, |
| 44 | ) |
| 45 | inlines = (forms.inlineformset_factory(User, AnotherModelWithLinkToUser), forms.inlineformset_factory(User, LastModelWithLinkToUser)) |
| 46 | }}} |
| 47 | |
| 48 | Method has_fieldsets would be changed to return True if there are fieldsets or inlines. Method fieldsets would be changed to iterate dictionaries which could have item "inline" which would be relevant inline formset instance - or not, it means that it is a fieldset. Each item in dictionaries would have items "counter" and "counter0" (maybe others variables from tag "for") with indexes local to type (inline or fieldset). So the example would like as: |
| 49 | |
| 50 | {{{ |
| 51 | ( |
| 52 | ('counter0': 0, 'counter': 1, 'fields': (...)), |
| 53 | ('counter0': 0, 'counter': 1, 'inline': ...), |
| 54 | ('counter0': 1, 'counter': 2, 'fields': (...), 'legend': 'Permission'), |
| 55 | ('counter0': 1, 'counter': 2, 'inline': ...), |
| 56 | ('counter0': 2, 'counter': 3, 'inline': ...), |
| 57 | ) |
| 58 | }}} |
| 59 | |
| 60 | All relevant methods of forms would be changed to iterate through inlines: is_valid, save, is_multipart and media. |
| 61 | |
| 62 | == Better API for formsets == |
| 63 | |
| 64 | Forms in formsets can be classified as: |
| 65 | * based on existing object or new, |
| 66 | * filled or not filled, |
| 67 | * valid or not valid, |
| 68 | * marked or not marked for deletion - if can_delete is True, |
| 69 | * ordered or not ordered - if can_order is True. |
| 70 | |
| 71 | So there are many combinations of states - but problem is that not all formset methods are consistent. For example, method is_valid does not check validity if form is marked for deletion, but cleaned_data property returns cleaned_data from all forms, possible not existing attribute. More generally, all forms are iterated in four methods - _deleted_forms, _ordered_forms, is_valid and full_clean - with different rules (if I really understand code, method is_valid iterates forms, in the first form checks errors property which calls methods full_clean which iterates all forms). And what I see as the main problem - there is no property of unordered formset which give me all filled and valid forms and nothing more (something as ordered_forms for ordered formset). |
| 72 | |
| 73 | So I would like to propose: |
| 74 | * Move all validation to method full_clean - this method sets attributes _is_valid, _errors, _non_form_errors, _cleaned_forms (if valid) and _deleted_forms (if valid and can_delete is True). If can_order is True, _cleaned_forms are ordered. |
| 75 | * Method is_valid only checks if full_clean has been run (if _errors is not None) and returns _is_valid. |
| 76 | * Property cleaned_forms only checks if full_clean has been run (if _errors is not None) and returns _cleaned_forms (it could raise !AttributeError if formset is not valid). |
| 77 | * Property deleted_forms only checks if full_clean has been run (if _errors is not None) and returns _deleted_forms (it could raise !AttributeError if formset is not valid). |
| 78 | * Property cleaned_data is deprecated - it should not be used because it could raise !AttributeError for valid formsets (and it is possible to iterate attribute forms for advanced use). |
| 79 | * Property ordered_forms is deprecated name for cleaned_forms. |
| 80 | * If form is marked for deletion (and can_delete is True) - it does not matter if is filled or valid. These forms are added to _deleted_forms. |
| 81 | * If form is not marked for deletion (or can_delete is False) and based on existing object, it has to be filled and valid, otherwise formset is not valid. These forms are added to _cleaned_forms. |
| 82 | * If form is not marked for deletion (or can_delete is False) and new, it has to be not filled or valid, otherwise formset is not valid. These forms are added to _cleaned_forms if filled. |
| 83 | |
| 84 | == Model fields with not-default values == |
| 85 | |
| 86 | I thing that this idea has been proposed - but I do not remember refusing, only not accepting. So I would like to propose attribute of inner meta class of model forms which could be called for example fields_kwargs. It would be dictionaries of redefined kwargs for fields. Example: |
| 87 | |
| 88 | {{{ |
| 89 | class UserForm(forms.ModelForm): |
| 90 | class Meta: |
| 91 | model = User |
| 92 | fields_kwargs = { |
| 93 | 'password': {'help_text': 'At least six characters.'}, |
| 94 | 'username': {'label': 'Name', 'help_text': 'Name for login.'}, |
| 95 | } |
| 96 | }}} |
| 97 | |
| 98 | Some people propose something as: |
| 99 | |
| 100 | {{{ |
| 101 | class UserForm(forms.ModelForm): |
| 102 | password = User._meta.get_field('password').formfield(help_text='At least six characters.') |
| 103 | username = helper_function(User, 'username', label='Name', help_text='Name for login.') |
| 104 | |
| 105 | class Meta: |
| 106 | model = User |
| 107 | }}} |
| 108 | |
| 109 | I do not like this. It is not DRY - it is necessary to write name of attribute twice. And what is more important - it hides indentions of author, it seems that fields are replaced but they are only changed a little. |
| 110 | |
| 111 | == Parameters "template" and "attrs" for forms, formsets, fieldsets, forms, (bound) fields and widgets == |
| 112 | |
| 113 | There are two requirements which seem to be conflicting: |
| 114 | * It should be possible to write generic templates which are able to render all forms and formsets. |
| 115 | * It should be possible to customize each part of form rendering - but only these parts which are necessary to customize. |
| 116 | |
| 117 | I would like to propose to add attributes "template" and "attrs" to all parts of forms - forms, formsets, fieldsets, (bound) fields and widgets. Attribute "attrs" should be dictionary of attributes for HTML code, attribute "template" should be name of template. These attributes would not be used in Django in any way, they would be only saved for template authors. They could write something as: |
| 118 | |
| 119 | {{{ |
| 120 | {% for field in form %} |
| 121 | {% if field.template %} |
| 122 | {% include field.template %} |
| 123 | {% else %} |
| 124 | <div {{ field.attrs|flatatt }}>{{ field.errors }} {{ field.label_tag }}: {{ field }}</div> |
| 125 | {% endif %} |
| 126 | {% endfor %} |
| 127 | }}} |
| 128 | |
| 129 | I do not propose how exactly these attributes should be set (it could be in inner meta class or in init or as class attributes or in factories), it is the next step if this idea would be accepted. |
| 130 | |
| 131 | == Support for (X)HTML5 and legacy HTML == |
| 132 | |
| 133 | This is quite new idea and I did not think about it so much as about other ones - but it is logical extension of the previous one. Django should support HTML5 forms in the future - but not all people would like to start using them in the same time (it would also break some tests). And others would like to use legacy HTML, not XTHML. |
| 134 | |
| 135 | What if widget rendering would not be hard-coded in Python code - but there would be only names of templates? And there should be some prepared templates - one set for HTML5, one set as in current Django. Everybody could decide which set to use or to write own set. |
| 136 | |
| 137 | I have no answer for these possible problems: |
| 138 | * How to guarantee that at least one set of templates is usable and form rendering is not broken? |
| 139 | * What about performance? Now caching loaders should be used in production - but is it enough? |
| 140 | |
| 141 | == Request in forms == |
| 142 | |
| 143 | (This proposal is rather request for design decision: I expect answer "no".) |
| 144 | |
| 145 | Quite often it is useful to use request in forms methods, especially in init. And it is also useful and simple to use generic views for forms. So it would be nice to have some generic mechanism for setting of request to form. My proposal is to add variable "request" to init method of forms and formsets with default value None - this variable would be only saved, not used in any way. |
| 146 | |
| 147 | This change could be again Django philosophy of "loose coupling" - forms should not know about requests. Requests in forms could be bad for testing of forms or for using of forms in management commands. But in reality many forms know about requests. |
| 148 | |
| 149 | (And I naturally know some possibilities how to solve this problem - for example request as thread local variable or factory method for form. But I do not know any nice and simple solution - maybe there is one.) |