forked from ietf-tools/datatracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfields.py
More file actions
130 lines (105 loc) · 4.61 KB
/
fields.py
File metadata and controls
130 lines (105 loc) · 4.61 KB
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
import json
from collections import namedtuple
from django import forms
from ietf.name.models import SessionPurposeName, TimeSlotTypeName
import debug # pyflakes: ignore
class SessionPurposeAndTypeWidget(forms.MultiWidget):
css_class = 'session_purpose_widget' # class to apply to all widgets
def __init__(self, purpose_choices, type_choices, *args, **kwargs):
# Avoid queries on models that need to be migrated into existence - this widget is
# instantiated during Django setup. Attempts to query, e.g., SessionPurposeName will
# prevent migrations from running.
widgets = (
forms.Select(
choices=purpose_choices,
attrs={
'class': self.css_class,
},
),
forms.Select(
choices=type_choices,
attrs={
'class': self.css_class,
'data-allowed-options': None,
},
),
)
super().__init__(widgets=widgets, *args, **kwargs)
# These queryset properties are needed to propagate changes to the querysets after initialization
# down to the widgets. The usual mechanisms in the ModelChoiceFields don't handle this for us
# because the subwidgets are not attached to Fields in the usual way.
@property
def purpose_choices(self):
return self.widgets[0].choices
@purpose_choices.setter
def purpose_choices(self, value):
self.widgets[0].choices = value
@property
def type_choices(self):
return self.widgets[1].choices
@type_choices.setter
def type_choices(self, value):
self.widgets[1].choices = value
def render(self, *args, **kwargs):
# Fill in the data-allowed-options (could not do this in init because it needs to
# query SessionPurposeName, which will break the migration if done during initialization)
self.widgets[1].attrs['data-allowed-options'] = json.dumps(self._allowed_types())
return super().render(*args, **kwargs)
def decompress(self, value):
if value:
return [getattr(val, 'pk', val) for val in value]
else:
return [None, None]
class Media:
js = ('secr/js/session_purpose_and_type_widget.js',)
def _allowed_types(self):
"""Map from purpose to allowed type values"""
return {
purpose.slug: list(purpose.timeslot_types)
for purpose in SessionPurposeName.objects.all()
}
class SessionPurposeAndTypeField(forms.MultiValueField):
"""Field to update Session purpose and type
Uses SessionPurposeAndTypeWidget to coordinate setting the session purpose and type to valid
combinations. Its value should be a tuple with (purpose, type). Its cleaned value is a
namedtuple with purpose and value properties.
"""
def __init__(self, purpose_queryset=None, type_queryset=None, **kwargs):
if purpose_queryset is None:
purpose_queryset = SessionPurposeName.objects.none()
if type_queryset is None:
type_queryset = TimeSlotTypeName.objects.none()
fields = (
forms.ModelChoiceField(queryset=purpose_queryset, label='Purpose'),
forms.ModelChoiceField(queryset=type_queryset, label='Type'),
)
self.widget = SessionPurposeAndTypeWidget(*(field.choices for field in fields))
super().__init__(fields=fields, **kwargs)
@property
def purpose_queryset(self):
return self.fields[0].queryset
@purpose_queryset.setter
def purpose_queryset(self, value):
self.fields[0].queryset = value
self.widget.purpose_choices = self.fields[0].choices
@property
def type_queryset(self):
return self.fields[1].queryset
@type_queryset.setter
def type_queryset(self, value):
self.fields[1].queryset = value
self.widget.type_choices = self.fields[1].choices
def compress(self, data_list):
# Convert data from the cleaned list from the widget into a namedtuple
if data_list:
compressed = namedtuple('CompressedSessionPurposeAndType', 'purpose type')
return compressed(*data_list)
return None
def validate(self, value):
# Additional validation - value has been passed through compress() already
if value.type.pk not in value.purpose.timeslot_types:
raise forms.ValidationError(
'"%(type)s" is not an allowed type for the purpose "%(purpose)s"',
params={'type': value.type, 'purpose': value.purpose},
code='invalid_type',
)