1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from flask
import request
18 from flask_wtf
import Form
19 from wtforms
.ext
.sqlalchemy
.fields
import QuerySelectField
20 from wtforms
.fields
.core
import FieldList
21 from wtforms
.widgets
.core
import HiddenInput
23 from indico
.core
.auth
import multipass
24 from indico
.util
.string
import strip_whitespace
27 class _DataWrapper(object):
28 """Wrapper for the return value of generated_data properties"""
29 def __init__(self
, data
):
33 return '<DataWrapper({!r})>'.format(self
.data
)
36 class generated_data(property):
37 """property decorator for generated data in forms"""
39 def __get__(self
, obj
, objtype
=None):
43 raise AttributeError("unreadable attribute")
44 return _DataWrapper(self
.fget(obj
))
47 class IndicoForm(Form
):
49 def bind_field(self
, form
, unbound_field
, options
):
50 # We don't set default filters for query-based fields as it breaks them if no query_factory is set
51 # while the Form is instantiated. Also, it's quite pointless for those fields...
52 # FieldList simply doesn't support filters.
53 no_filter_fields
= (QuerySelectField
, FieldList
)
54 filters
= [strip_whitespace
] if not issubclass(unbound_field
.field_class
, no_filter_fields
) else []
55 filters
+= unbound_field
.kwargs
.get('filters', [])
56 return unbound_field
.bind(form
=form
, filters
=filters
, **options
)
58 def populate_obj(self
, obj
, fields
=None, skip
=None, existing_only
=False):
59 """Populates the given object with form data.
61 If `fields` is set, only fields from that list are populated.
62 If `skip` is set, fields in that list are skipped.
63 If `existing_only` is True, only attributes that already exist on `obj` are populated.
65 for name
, field
in self
._fields
.iteritems():
66 if fields
and name
not in fields
:
68 if skip
and name
in skip
:
70 if existing_only
and not hasattr(obj
, name
):
72 field
.populate_obj(obj
, name
)
75 def visible_fields(self
):
76 """A list containing all fields that are not hidden."""
77 return [field
for field
in self
if not isinstance(field
.widget
, HiddenInput
)]
81 """A list containing all errors, prefixed with the field's label.'"""
83 for field_name
, errors
in self
.errors
.iteritems():
85 if isinstance(error
, dict) and isinstance(self
[field_name
], FieldList
):
86 for field
in self
[field_name
].entries
:
87 all_errors
+= ['{}: {}'.format(self
[field_name
].label
.text
, sub_error
)
88 for sub_error
in field
.form
.error_list
]
90 all_errors
.append('{}: {}'.format(self
[field_name
].label
.text
, error
))
95 """Extends form.data with generated data from properties"""
96 data
= super(IndicoForm
, self
).data
98 for field
in dir(cls
):
99 if isinstance(getattr(cls
, field
), generated_data
):
100 data
[field
] = getattr(self
, field
).data
104 class FormDefaults(object):
105 """Simple wrapper to be used for Form(obj=...) default values.
107 It allows you to specify default values via kwargs or certain attrs from an object.
108 You can also set attributes directly on this object, which will act just like kwargs
110 :param obj: The object to get data from
111 :param attrs: Set of attributes that may be taken from obj
112 :param skip_attrs: Set of attributes which are never taken from obj
113 :param defaults: Additional values which are used only if not taken from obj
116 def __init__(self
, obj
=None, attrs
=None, skip_attrs
=None, **defaults
):
118 self
.__use
_items
= hasattr(obj
, 'iteritems') and hasattr(obj
, 'get') # smells like a dict
119 self
.__obj
_attrs
= attrs
120 self
.__obj
_attrs
_skip
= skip_attrs
121 self
.__defaults
= defaults
123 def __valid_attr(self
, name
):
124 """Checks if an attr may be retrieved from the object"""
125 if self
.__obj
is None:
127 if self
.__obj
_attrs
is not None and name
not in self
.__obj
_attrs
:
129 if self
.__obj
_attrs
_skip
is not None and name
in self
.__obj
_attrs
_skip
:
133 def __setitem__(self
, key
, value
):
134 self
.__defaults
[key
] = value
136 def __setattr__(self
, key
, value
):
137 if key
.startswith('_{}__'.format(type(self
).__name
__)):
138 object.__setattr
__(self
, key
, value
)
140 self
.__defaults
[key
] = value
142 def __getattr__(self
, item
):
143 if self
.__valid
_attr
(item
):
145 return self
.__obj
.get(item
, self
.__defaults
.get(item
))
147 return getattr(self
.__obj
, item
, self
.__defaults
.get(item
))
148 elif item
in self
.__defaults
:
149 return self
.__defaults
[item
]
151 raise AttributeError(item
)
154 class SyncedInputsMixin(object):
155 """Mixin for a form having inputs using the ``SyncedInputWidget``.
157 This mixin will process the synced fields, adding them the necessary
158 attributes for them to render and work properly. The fields which
159 are synced are defined by ``multipass.synced_fields``.
161 :param synced_fields: set -- a subset of ``multipass.synced_fields``
162 which corresponds to the fields currently
163 being synchronized for the user.
164 :param synced_values: dict -- a map of all the synced fields (as
165 defined by ``multipass.synced_fields``) and
166 the values they would have if they were synced
167 (regardless of whether it is or not). Fields
168 not present in this dict do not show the sync
172 def __init__(self
, *args
, **kwargs
):
173 synced_fields
= kwargs
.pop('synced_fields', set())
174 synced_values
= kwargs
.pop('synced_values', {})
175 super(SyncedInputsMixin
, self
).__init
__(*args
, **kwargs
)
176 self
.syncable_fields
= set(synced_values
)
177 for key
in ('first_name', 'last_name'):
178 if not synced_values
.get(key
):
179 synced_values
.pop(key
, None)
180 self
.syncable_fields
.discard(key
)
181 if self
.is_submitted():
182 synced_fields
= self
.synced_fields
183 provider
= multipass
.sync_provider
184 provider_name
= provider
.title
if provider
is not None else 'unknown identity provider'
185 for field
in multipass
.synced_fields
:
186 self
[field
].synced
= self
[field
].short_name
in synced_fields
187 self
[field
].synced_value
= synced_values
.get(field
)
188 self
[field
].provider_name
= provider_name
191 def synced_fields(self
):
192 """The fields which are set as synced for the current request."""
193 return set(request
.form
.getlist('synced_fields')) & self
.syncable_fields