forked from apluslms/a-plus
-
Notifications
You must be signed in to change notification settings - Fork 0
/
widgets.py
178 lines (152 loc) · 7.08 KB
/
widgets.py
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
from typing import Any, Dict, List, Optional, Tuple
from django import forms
from django.template import Variable
class DateTimeLocalInput(forms.DateTimeInput):
"""
A datetime widget that uses the `datetime-local` input type in HTML.
The initial value of the HTML input must be formatted `YYYY-MM-DDThh:mm`.
This widget ensures proper formatting.
The submitted form also uses this format, which Django can already handle
without custom logic (see `django.utils.dateparse.datetime_re`).
"""
input_type = 'datetime-local'
def __init__(self, attrs: Optional[Dict[str, Any]] = None) -> None:
default_attrs = {'step': 1} # Display seconds in widget
if attrs is not None:
default_attrs.update(attrs)
super().__init__(attrs=default_attrs, format='%Y-%m-%dT%H:%M')
class DurationInput(forms.MultiWidget):
"""
A widget for entering a duration of time.
Renders as a row of text boxes, one for each given unit of time (e.g. days,
hours and minutes).
Use with `lib.fields.DurationField`.
"""
units: List[Tuple[str, int]]
def __init__(self, units: List[Tuple[str, int]], attrs: Optional[Dict[str, Any]] = None) -> None:
self.units = units
widgets = [forms.NumberInput({'placeholder': name}) for name, factor in self.units]
default_attrs = {'class': 'duration-input'}
if attrs is not None:
default_attrs.update(attrs)
super().__init__(widgets, default_attrs)
def decompress(self, value: Optional[int]) -> List[Optional[int]]:
"""
Converts the given minute value into the different units.
"""
if value is None:
return [None] * len(self.units)
remainder = value
unit_values = []
for _name, factor in self.units:
unit_values.append(remainder // factor)
remainder = remainder % factor
return unit_values
def value_from_datadict(self, data: Dict[str, Any], files: Dict[str, List[Any]], name: str) -> Any:
"""
Extract every individual unit's value from a submitted data dictionary.
If instead of different units, a single value is submitted (i.e.
request was probably not made using a HTML form), use that as the base
unit and set every other unit to None.
"""
if name in data:
value = [None] * len(self.units)
value[-1] = data[name]
return value
return super().value_from_datadict(data, files, name)
class SearchSelect(forms.SelectMultiple):
"""
A multi-select widget with search, copy and paste functionality.
Explanations of the initialization parameters:
* `ajax`: whether the widget should query the API to get suggestions.
* `display_fields`: names of fields that comprise the display name of an
instance. If not provided, the field's `label_from_instance` method will
be used to create the display name.
* `clipboard_fields`: names of the fields whose values can be copied and
pasted as a comma-separated list. If not provided, only the IDs of the
selected instances can be copied and pasted.
* `field_sources`: maps field names to their sources. Provide sources for
the fields that do not exist in the instance. The source string uses the
same syntax as Django templates.
* `field_labels`: maps field names to their labels. The label appears in
the copy/paste menus.
* `search_api_url`: URL of the API endpoint that will be queried if `ajax`
is `True`. The field names mentioned in the previous parameters should
match the fields returned by this endpoint. The endpoint must support the
"search", "field" and "values" GET query parameters since those are used
in queries.
"""
template_name = 'ajax_search_select.html'
class Media:
js = ('js/ajax_search_select.js',)
def __init__( # pylint: disable=too-many-arguments keyword-arg-before-vararg
self,
ajax: bool = False,
display_fields: Optional[List[str]] = None,
clipboard_fields: Optional[List[str]] = None,
field_sources: Optional[Dict[str, str]] = None,
field_labels: Optional[Dict[str, str]] = None,
search_api_url: Optional[str] = None,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self.ajax = ajax
self.display_fields = display_fields or []
self.clipboard_fields = clipboard_fields or []
self.field_sources = field_sources or {}
self.field_labels = field_labels or {}
self.search_api_url = search_api_url
def get_context(self, name: str, value: Any, attrs: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""
Get data used to render the widget.
Overrides `Widget.get_context`.
"""
context = super().get_context(name, value, attrs)
# Keep the attributes of this widget and the inner widget and separate.
context['inner_widget'] = context['widget']
inner_id = context['inner_widget']['attrs']['id']
context['widget'] = {
'attrs': {
'id': inner_id + '_wrapper',
'class': 'search-select-ajax' if self.ajax else 'search-select',
'data-display-fields': ','.join(self.display_fields),
'data-clipboard-fields': ','.join(self.clipboard_fields),
'data-search-api-url': self.search_api_url or '',
},
'input_id': inner_id,
}
context['clipboard_options'] = [
{'field': field, 'label': self.field_labels.get(field, field)}
for field in self.clipboard_fields
]
context['inner_widget']['attrs']['id'] = inner_id + '_select'
return context
def create_option(self, name: str, value: Any, *args: Any, **kwargs: Any) -> Dict[str, Any]:
"""
Get the data used to render a single option in the widget.
Overrides `ChoiceWidget.create_option`.
"""
result = super().create_option(name, value, *args, **kwargs)
# Replace the label with a custom one, if display fields are provided.
if self.display_fields:
result['label'] = ", ".join((
self.get_field_value(value.instance, field)
for field in self.display_fields
))
# Add the data-* attributes that are used for copy/paste functionality.
result['attrs'].update({
f'data-{field}': self.get_field_value(value.instance, field)
for field in self.clipboard_fields
})
return result
def get_field_value(self, instance: Any, field: str) -> str:
"""
Get the value of a field in an object as a string.
"""
if field in self.field_sources:
# Use the same syntax as Django templates for the field source.
value = Variable(self.field_sources[field]).resolve(instance)
else:
value = getattr(instance, field, None)
return str(value) if value is not None else ''