Code

Changes between Initial Version and Version 1 of ImprovementsForDjangoForms


Ignore:
Timestamp:
06/03/10 04:38:06 (4 years ago)
Author:
Petr Marhoun <petr.marhoun@…>
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • ImprovementsForDjangoForms

    v1 v1  
     1= Proposal: Improvements for django.forms = 
     2 
     3I 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 
     5These 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 
     10My 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 
     12And sorry for my English. 
     13 
     14== Fieldsets == 
     15 
     16I 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{{{ 
     19class 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 
     28Forms 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 
     30Fieldsets 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 
     34I 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{{{ 
     37class 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 
     48Method 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 
     60All 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 
     64Forms 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 
     71So 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 
     73So 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 
     86I 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{{{ 
     89class 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 
     98Some people propose something as: 
     99 
     100{{{ 
     101class 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 
     109I 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 
     113There 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 
     117I 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 
     129I 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 
     133This 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 
     135What 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 
     137I 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 
     145Quite 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 
     147This 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.)