Do not allow signup with empty first/last name
[cds-indico.git] / indico / web / forms / base.py
blob0590a86df8e07e9c6f89e42286edf20e4b36d7dc
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):
30 self.data = data
32 def __repr__(self):
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):
40 if obj is None:
41 return self
42 if self.fget is None:
43 raise AttributeError("unreadable attribute")
44 return _DataWrapper(self.fget(obj))
47 class IndicoForm(Form):
48 class Meta:
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.
64 """
65 for name, field in self._fields.iteritems():
66 if fields and name not in fields:
67 continue
68 if skip and name in skip:
69 continue
70 if existing_only and not hasattr(obj, name):
71 continue
72 field.populate_obj(obj, name)
74 @property
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)]
79 @property
80 def error_list(self):
81 """A list containing all errors, prefixed with the field's label.'"""
82 all_errors = []
83 for field_name, errors in self.errors.iteritems():
84 for error in errors:
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]
89 else:
90 all_errors.append('{}: {}'.format(self[field_name].label.text, error))
91 return all_errors
93 @property
94 def data(self):
95 """Extends form.data with generated data from properties"""
96 data = super(IndicoForm, self).data
97 cls = type(self)
98 for field in dir(cls):
99 if isinstance(getattr(cls, field), generated_data):
100 data[field] = getattr(self, field).data
101 return 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):
117 self.__obj = obj
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:
126 return False
127 if self.__obj_attrs is not None and name not in self.__obj_attrs:
128 return False
129 if self.__obj_attrs_skip is not None and name in self.__obj_attrs_skip:
130 return False
131 return True
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)
139 else:
140 self.__defaults[key] = value
142 def __getattr__(self, item):
143 if self.__valid_attr(item):
144 if self.__use_items:
145 return self.__obj.get(item, self.__defaults.get(item))
146 else:
147 return getattr(self.__obj, item, self.__defaults.get(item))
148 elif item in self.__defaults:
149 return self.__defaults[item]
150 else:
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
169 button at all.
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
190 @property
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