summaryrefslogtreecommitdiff
path: root/django/forms/utils.py
blob: f4fbf3e2418a23ff1933766ccdd669986237ef58 (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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import json
from collections import UserList

from django.conf import settings
from django.core.exceptions import ValidationError
from django.forms.renderers import get_default_renderer
from django.utils import timezone
from django.utils.html import escape, format_html_join
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _


def pretty_name(name):
    """Convert 'first_name' to 'First name'."""
    if not name:
        return ""
    return name.replace("_", " ").capitalize()


def flatatt(attrs):
    """
    Convert a dictionary of attributes to a single string.
    The returned string will contain a leading space followed by key="value",
    XML-style pairs. In the case of a boolean value, the key will appear
    without a value. It is assumed that the keys do not need to be
    XML-escaped. If the passed dictionary is empty, then return an empty
    string.

    The result is passed through 'mark_safe' (by way of 'format_html_join').
    """
    key_value_attrs = []
    boolean_attrs = []
    for attr, value in attrs.items():
        if isinstance(value, bool):
            if value:
                boolean_attrs.append((attr,))
        elif value is not None:
            key_value_attrs.append((attr, value))

    return format_html_join("", ' {}="{}"', sorted(key_value_attrs)) + format_html_join(
        "", " {}", sorted(boolean_attrs)
    )


class RenderableMixin:
    def get_context(self):
        raise NotImplementedError(
            "Subclasses of RenderableMixin must provide a get_context() method."
        )

    def render(self, template_name=None, context=None, renderer=None):
        renderer = renderer or self.renderer
        template = template_name or self.template_name
        context = context or self.get_context()
        return mark_safe(renderer.render(template, context))

    __str__ = render
    __html__ = render


class RenderableFieldMixin(RenderableMixin):
    def as_field_group(self):
        return self.render()

    def as_hidden(self):
        raise NotImplementedError(
            "Subclasses of RenderableFieldMixin must provide an as_hidden() method."
        )

    def as_widget(self):
        raise NotImplementedError(
            "Subclasses of RenderableFieldMixin must provide an as_widget() method."
        )

    def __str__(self):
        """Render this field as an HTML widget."""
        if self.field.show_hidden_initial:
            return self.as_widget() + self.as_hidden(only_initial=True)
        return self.as_widget()

    __html__ = __str__


class RenderableFormMixin(RenderableMixin):
    def as_p(self):
        """Render as <p> elements."""
        return self.render(self.template_name_p)

    def as_table(self):
        """Render as <tr> elements excluding the surrounding <table> tag."""
        return self.render(self.template_name_table)

    def as_ul(self):
        """Render as <li> elements excluding the surrounding <ul> tag."""
        return self.render(self.template_name_ul)

    def as_div(self):
        """Render as <div> elements."""
        return self.render(self.template_name_div)


class RenderableErrorMixin(RenderableMixin):
    def as_json(self, escape_html=False):
        return json.dumps(self.get_json_data(escape_html))

    def as_text(self):
        return self.render(self.template_name_text)

    def as_ul(self):
        return self.render(self.template_name_ul)


class ErrorDict(dict, RenderableErrorMixin):
    """
    A collection of errors that knows how to display itself in various formats.

    The dictionary keys are the field names, and the values are the errors.
    """

    template_name = "django/forms/errors/dict/default.html"
    template_name_text = "django/forms/errors/dict/text.txt"
    template_name_ul = "django/forms/errors/dict/ul.html"

    def __init__(self, *args, renderer=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.renderer = renderer or get_default_renderer()

    def as_data(self):
        return {f: e.as_data() for f, e in self.items()}

    def get_json_data(self, escape_html=False):
        return {f: e.get_json_data(escape_html) for f, e in self.items()}

    def get_context(self):
        return {
            "errors": self.items(),
            "error_class": "errorlist",
        }


class ErrorList(UserList, list, RenderableErrorMixin):
    """
    A collection of errors that knows how to display itself in various formats.
    """

    template_name = "django/forms/errors/list/default.html"
    template_name_text = "django/forms/errors/list/text.txt"
    template_name_ul = "django/forms/errors/list/ul.html"

    def __init__(self, initlist=None, error_class=None, renderer=None):
        super().__init__(initlist)

        if error_class is None:
            self.error_class = "errorlist"
        else:
            self.error_class = "errorlist {}".format(error_class)
        self.renderer = renderer or get_default_renderer()

    def as_data(self):
        return ValidationError(self.data).error_list

    def copy(self):
        copy = super().copy()
        copy.error_class = self.error_class
        return copy

    def get_json_data(self, escape_html=False):
        errors = []
        for error in self.as_data():
            message = next(iter(error))
            errors.append(
                {
                    "message": escape(message) if escape_html else message,
                    "code": error.code or "",
                }
            )
        return errors

    def get_context(self):
        return {
            "errors": self,
            "error_class": self.error_class,
        }

    def __repr__(self):
        return repr(list(self))

    def __contains__(self, item):
        return item in list(self)

    def __eq__(self, other):
        return list(self) == other

    def __getitem__(self, i):
        error = self.data[i]
        if isinstance(error, ValidationError):
            return next(iter(error))
        return error

    def __reduce_ex__(self, *args, **kwargs):
        # The `list` reduce function returns an iterator as the fourth element
        # that is normally used for repopulating. Since we only inherit from
        # `list` for `isinstance` backward compatibility (Refs #17413) we
        # nullify this iterator as it would otherwise result in duplicate
        # entries. (Refs #23594)
        info = super(UserList, self).__reduce_ex__(*args, **kwargs)
        return info[:3] + (None, None)


# Utilities for time zone support in DateTimeField et al.


def from_current_timezone(value):
    """
    When time zone support is enabled, convert naive datetimes
    entered in the current time zone to aware datetimes.
    """
    if settings.USE_TZ and value is not None and timezone.is_naive(value):
        current_timezone = timezone.get_current_timezone()
        try:
            if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
                raise ValueError("Ambiguous or non-existent time.")
            return timezone.make_aware(value, current_timezone)
        except Exception as exc:
            raise ValidationError(
                _(
                    "%(datetime)s couldn’t be interpreted "
                    "in time zone %(current_timezone)s; it "
                    "may be ambiguous or it may not exist."
                ),
                code="ambiguous_timezone",
                params={"datetime": value, "current_timezone": current_timezone},
            ) from exc
    return value


def to_current_timezone(value):
    """
    When time zone support is enabled, convert aware datetimes
    to naive datetimes in the current time zone for display.
    """
    if settings.USE_TZ and value is not None and timezone.is_aware(value):
        return timezone.make_naive(value)
    return value