summaryrefslogtreecommitdiff
path: root/django/newforms/forms.py
blob: e490d0d5f9d43aec303acdf1ce584330c701621d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""
Form classes
"""

from fields import Field
from widgets import TextInput, Textarea
from util import ErrorDict, ErrorList, ValidationError

NON_FIELD_ERRORS = '__all__'

def pretty_name(name):
    "Converts 'first_name' to 'First name'"
    name = name[0].upper() + name[1:]
    return name.replace('_', ' ')

class DeclarativeFieldsMetaclass(type):
    "Metaclass that converts Field attributes to a dictionary called 'fields'."
    def __new__(cls, name, bases, attrs):
        attrs['fields'] = dict([(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)])
        return type.__new__(cls, name, bases, attrs)

class Form(object):
    "A collection of Fields, plus their associated data."
    __metaclass__ = DeclarativeFieldsMetaclass

    def __init__(self, data=None): # TODO: prefix stuff
        self.data = data or {}
        self.clean_data = None # Stores the data after clean() has been called.
        self.__errors = None # Stores the errors after clean() has been called.

    def __str__(self):
        return self.as_table()

    def __iter__(self):
        for name, field in self.fields.items():
            yield BoundField(self, field, name)

    def __getitem__(self, name):
        "Returns a BoundField with the given name."
        try:
            field = self.fields[name]
        except KeyError:
            raise KeyError('Key %r not found in Form' % name)
        return BoundField(self, field, name)

    def clean(self):
        if self.__errors is None:
            self.full_clean()
        return self.clean_data

    def errors(self):
        "Returns an ErrorDict for self.data"
        if self.__errors is None:
            self.full_clean()
        return self.__errors

    def is_valid(self):
        """
        Returns True if the form has no errors. Otherwise, False. This exists
        solely for convenience, so client code can use positive logic rather
        than confusing negative logic ("if not form.errors()").
        """
        return not bool(self.errors())

    def as_table(self):
        "Returns this form rendered as an HTML <table>."
        output = u'\n'.join(['<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()])
        return '<table>\n%s\n</table>' % output

    def as_ul(self):
        "Returns this form rendered as an HTML <ul>."
        output = u'\n'.join(['<li>%s: %s</li>' % (pretty_name(name), BoundField(self, field, name)) for name, field in self.fields.items()])
        return '<ul>\n%s\n</ul>' % output

    def as_table_with_errors(self):
        "Returns this form rendered as an HTML <table>, with errors."
        output = []
        if self.errors().get(NON_FIELD_ERRORS):
            # Errors not corresponding to a particular field are displayed at the top.
            output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]]))
        for name, field in self.fields.items():
            bf = BoundField(self, field, name)
            if bf.errors:
                output.append('<tr><td colspan="2"><ul>%s</ul></td></tr>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors]))
            output.append('<tr><td>%s:</td><td>%s</td></tr>' % (pretty_name(name), bf))
        return '<table>\n%s\n</table>' % '\n'.join(output)

    def as_ul_with_errors(self):
        "Returns this form rendered as an HTML <ul>, with errors."
        output = []
        if self.errors().get(NON_FIELD_ERRORS):
            # Errors not corresponding to a particular field are displayed at the top.
            output.append('<li><ul>%s</ul></li>' % '\n'.join(['<li>%s</li>' % e for e in self.errors()[NON_FIELD_ERRORS]]))
        for name, field in self.fields.items():
            bf = BoundField(self, field, name)
            line = '<li>'
            if bf.errors:
                line += '<ul>%s</ul>' % '\n'.join(['<li>%s</li>' % e for e in bf.errors])
            line += '%s: %s</li>' % (pretty_name(name), bf)
            output.append(line)
        return '<ul>\n%s\n</ul>' % '\n'.join(output)

    def full_clean(self):
        """
        Cleans all of self.data and populates self.__errors and self.clean_data.
        """
        self.clean_data = {}
        errors = ErrorDict()
        for name, field in self.fields.items():
            value = self.data.get(name, None)
            try:
                value = field.clean(value)
                self.clean_data[name] = value
                if hasattr(self, 'clean_%s' % name):
                    value = getattr(self, 'clean_%s' % name)()
                self.clean_data[name] = value
            except ValidationError, e:
                errors[name] = e.messages
        try:
            self.clean_data = self.clean()
        except ValidationError, e:
            errors[NON_FIELD_ERRORS] = e.messages
        if errors:
            self.clean_data = None
        self.__errors = errors

    def clean(self):
        """
        Hook for doing any extra form-wide cleaning after Field.clean() been
        called on every field.
        """
        return self.clean_data

class BoundField(object):
    "A Field plus data"
    def __init__(self, form, field, name):
        self._form = form
        self._field = field
        self._name = name

    def __str__(self):
        "Renders this field as an HTML widget."
        # Use the 'widget' attribute on the field to determine which type
        # of HTML widget to use.
        return self.as_widget(self._field.widget)

    def _errors(self):
        """
        Returns an ErrorList for this field. Returns an empty ErrorList
        if there are none.
        """
        try:
            return self._form.errors()[self._name]
        except KeyError:
            return ErrorList()
    errors = property(_errors)

    def as_widget(self, widget, attrs=None):
        return widget.render(self._name, self._form.data.get(self._name, None), attrs=attrs)

    def as_text(self, attrs=None):
        """
        Returns a string of HTML for representing this as an <input type="text">.
        """
        return self.as_widget(TextInput(), attrs)

    def as_textarea(self, attrs=None):
        "Returns a string of HTML for representing this as a <textarea>."
        return self.as_widget(Textarea(), attrs)