class fossirProtectionField(fossirEnumRadioField): widget = JinjaWidget('forms/protection_widget.html', single_kwargs=True) radio_widget = JinjaWidget('forms/radio_buttons_widget.html', orientation='horizontal', single_kwargs=True) def __init__(self, *args, **kwargs): self.protected_object = kwargs.pop('protected_object')(kwargs['_form']) get_acl_message_url = kwargs.pop('acl_message_url', None) self.acl_message_url = get_acl_message_url(kwargs['_form']) if get_acl_message_url else None self.can_inherit_protection = self.protected_object.protection_parent is not None if not self.can_inherit_protection: kwargs['skip'] = {ProtectionMode.inheriting} super(fossirProtectionField, self).__init__(*args, enum=ProtectionMode, **kwargs) def render_protection_message(self): protected_object = self.get_form().protected_object if hasattr(protected_object, 'get_non_inheriting_objects'): non_inheriting_objects = protected_object.get_non_inheriting_objects() else: non_inheriting_objects = [] if isinstance(protected_object.protection_parent, db.m.Event): parent_type = _('Event') elif isinstance(protected_object.protection_parent, db.m.Category): parent_type = _('Category') else: parent_type = _('Session') rv = render_template('_protection_info.html', field=self, protected_object=protected_object, parent_type=parent_type, non_inheriting_objects=non_inheriting_objects) return Markup(rv)
def make_review_form(event, review_type): """Extends the paper WTForm to add the extra fields. Each extra field will use a field named ``custom_ID``. :param event: The `Event` for which to create the paper review form. :param review_type: The `PaperReviewType` for which to create the paper review form. :return: A `PaperReviewForm` subclass. """ form_class = type(b'_PaperReviewForm', (PaperReviewForm, ), {}) for idx, question in enumerate( event.cfp.get_questions_for_review_type(review_type), start=1): name = 'question_{}'.format(question.id) range_ = event.cfp.rating_range field = RadioField(question.text, validators=[DataRequired()], choices=[(unicode(n), unicode(n)) for n in range(range_[0], range_[1] + 1)], widget=JinjaWidget( 'events/reviews/rating_widget.html', question=question, rating_range=event.cfp.rating_range, inline_js=True, question_idx=idx)) setattr(form_class, name, field) return form_class
class fossirPalettePickerField(JSONField): """Field allowing user to pick a color from a set of predefined values""" widget = JinjaWidget('forms/palette_picker_widget.html') CAN_POPULATE = True def __init__(self, *args, **kwargs): self.color_list = kwargs.pop('color_list') super(fossirPalettePickerField, self).__init__(*args, **kwargs) def pre_validate(self, form): if self.data not in self.color_list: raise ValueError(_('Invalid colors selected')) def process_formdata(self, valuelist): super(fossirPalettePickerField, self).process_formdata(valuelist) self.data = ColorTuple(self.data['text'], self.data['background']) def process_data(self, value): super(fossirPalettePickerField, self).process_data(value) if self.object_data and self.object_data not in self.color_list: self.color_list = self.color_list + [self.object_data] def _value(self): return self.data._asdict()
class EventPersonLinkListField(PersonLinkListFieldBase): """A field to manage event's chairpersons""" person_link_cls = EventPersonLink linked_object_attr = 'event' widget = JinjaWidget('events/forms/event_person_link_widget.html') def __init__(self, *args, **kwargs): self.allow_submitters = True self.default_is_submitter = kwargs.pop('default_is_submitter', True) super(EventPersonLinkListField, self).__init__(*args, **kwargs) def _convert_data(self, data): return { self._get_person_link(x): x.pop('isSubmitter', self.default_is_submitter) for x in data } def _serialize_person_link(self, principal, extra_data=None): extra_data = extra_data or {} data = dict(extra_data, **serialize_person_link(principal)) data['isSubmitter'] = self.data[principal] if self.get_form( ).is_submitted() else principal.is_submitter return data def pre_validate(self, form): super(PersonLinkListFieldBase, self).pre_validate(form) persons = set() for person_link in self.data: if person_link.person in persons: raise ValueError( _("Person with email '{}' is duplicated").format( person_link.person.email)) persons.add(person_link.person)
class fossirMarkdownField(TextAreaField): """A Markdown-enhanced textarea. When using the editor you need to include the markdown JS/CSS bundles and also the MathJax JS bundle (even when using only the editor without Mathjax). :param editor: Whether to use the WMD widget with its live preview :param mathjax: Whether to use MathJax in the WMD live preview """ widget = JinjaWidget('forms/markdown_widget.html', single_kwargs=True, rows=5) def __init__(self, *args, **kwargs): self.use_editor = kwargs.pop('editor', False) self.use_mathjax = kwargs.pop('mathjax', False) orig_id = kwargs['_prefix'] + kwargs['_name'] if self.use_editor: # WMD relies on this awful ID :/ kwargs['id'] = 'wmd-input-f_' + orig_id else: kwargs.setdefault( 'description', _(u"You can use Markdown or basic HTML formatting tags.")) super(fossirMarkdownField, self).__init__(*args, **kwargs) self.orig_id = orig_id
class fossirEmailRecipientsField(Field): widget = JinjaWidget('forms/email_recipients_widget.html', single_kwargs=True) def process_data(self, data): self.data = sorted(data, key=unicode.lower) self.text_value = ', '.join(data) self.count = len(data)
class AccessControlListField(PrincipalListField): widget = JinjaWidget('forms/principal_list_widget.html', single_kwargs=True, acl=True) def __init__(self, *args, **kwargs): # The text of the link that changes the protection mode of the object to protected self.default_text = kwargs.pop('default_text') super(AccessControlListField, self).__init__(*args, **kwargs)
class OccurrencesField(JSONField): """ A field that lets you select multiple occurrences consisting of a start date/time and a duration. """ widget = JinjaWidget('forms/occurrences_widget.html', single_line=True) CAN_POPULATE = True def __init__(self, *args, **kwargs): self._timezone = kwargs.pop('timezone', None) self.default_time = kwargs.pop('default_time', time(0, 0)) self.default_duration = kwargs.pop('default_duration', timedelta()) kwargs.setdefault('default', []) super(OccurrencesField, self).__init__(*args, **kwargs) def process_formdata(self, valuelist): def _deserialize(occ): try: dt = dateutil.parser.parse('{} {}'.format( occ['date'], occ['time'])) except ValueError: raise ValueError('Invalid date/time: {} {}'.format( escape(occ['date']), escape(occ['time']))) return localize_as_utc( dt, self.timezone), timedelta(minutes=occ['duration']) self.data = [] super(OccurrencesField, self).process_formdata(valuelist) self.data = map(_deserialize, self.data) def _value(self): def _serialize(occ): if isinstance(occ, dict): # raw data from the client return occ dt = occ[0].astimezone(pytz.timezone(self.timezone)) return { 'date': dt.date().isoformat(), 'time': dt.time().isoformat()[:-3], # hh:mm only 'duration': int(occ[1].total_seconds() // 60) } return json.dumps(map(_serialize, self.data)) @property def timezone_field(self): field = getattr(self.get_form(), 'timezone', None) return field if isinstance(field, SelectField) else None @property def timezone(self): if self.timezone_field: return self.timezone_field.data else: return getattr(self.get_form(), 'timezone', session.tzinfo.zone)
class fossirDateField(DateField): widget = JinjaWidget('forms/date_widget.html', single_line=True, single_kwargs=True) def __init__(self, *args, **kwargs): super(fossirDateField, self).__init__(*args, parse_kwargs={'dayfirst': True}, display_format='%d/%m/%Y', **kwargs)
class TrackRoleField(JSONField): """A field that stores a list of e-mail template rules.""" CAN_POPULATE = True widget = JinjaWidget('events/abstracts/forms/track_role_widget.html') @property def users(self): return { user_id: _serialize_user(user) for user_id, user in _get_users_in_roles(self.data) } @property def role_data(self): conveners = set() reviewers = set() # Handle global reviewers/conveners role_data = self.data.pop('*') global_conveners = _get_users(role_data['convener']) global_reviewers = _get_users(role_data['reviewer']) conveners |= global_conveners reviewers |= global_reviewers track_dict = { track.id: track for track in Track.query.with_parent(self.event).filter( Track.id.in_(self.data)) } user_dict = dict(_get_users_in_roles(self.data)) track_roles = {} # Update track-specific reviewers/conveners for track_id, roles in self.data.viewitems(): track = track_dict[int(track_id)] track_roles[track] = defaultdict(set) for role_id, user_ids in roles.viewitems(): users = {user_dict[user_id] for user_id in user_ids} track_roles[track][role_id] = users if role_id == 'convener': conveners |= users elif role_id == 'reviewer': reviewers |= users return { 'track_roles': track_roles, 'global_conveners': global_conveners, 'global_reviewers': global_reviewers, 'all_conveners': conveners, 'all_reviewers': reviewers } def _value(self): return super(TrackRoleField, self)._value() if self.data else '[]'
class SessionBlockPersonLinkListField(PersonLinkListFieldBase): person_link_cls = SessionBlockPersonLink linked_object_attr = 'session_block' widget = JinjaWidget('events/sessions/forms/session_person_link_widget.html') def _serialize_person_link(self, principal, extra_data=None): extra_data = extra_data or {} return dict(extra_data, **serialize_person_link(principal)) def _convert_data(self, data): return list({self._get_person_link(x) for x in data})
class PrincipalField(PrincipalListField): """A field that lets you select an fossir user/group ("principal")""" widget = JinjaWidget('forms/principal_widget.html', single_line=True) def _get_data(self): return [] if self.data is None else [self.data] def process_formdata(self, valuelist): if valuelist: data = map(self._convert_principal, json.loads(valuelist[0])) self.data = None if not data else data[0]
class AbstractPersonLinkListField(PersonLinkListFieldBase): """A field to configure a list of abstract persons""" person_link_cls = AbstractPersonLink linked_object_attr = 'abstract' default_sort_alpha = False create_untrusted_persons = True widget = JinjaWidget( 'events/contributions/forms/contribution_person_link_widget.html', allow_empty_email=True) def __init__(self, *args, **kwargs): self.author_types = AuthorType.serialize() self.allow_authors = True self.allow_submitters = False self.show_empty_coauthors = kwargs.pop('show_empty_coauthors', True) self.default_author_type = kwargs.pop('default_author_type', AuthorType.none) self.default_is_submitter = False self.default_is_speaker = False self.require_primary_author = True super(AbstractPersonLinkListField, self).__init__(*args, **kwargs) def _convert_data(self, data): return list({self._get_person_link(x) for x in data}) @no_autoflush def _get_person_link(self, data): extra_data = { 'author_type': data.pop('authorType', self.default_author_type), 'is_speaker': data.pop('isSpeaker', self.default_is_speaker) } return super(AbstractPersonLinkListField, self)._get_person_link(data, extra_data) def _serialize_person_link(self, principal, extra_data=None): extra_data = extra_data or {} data = dict(extra_data, **serialize_person_link(principal)) data['isSpeaker'] = principal.is_speaker data['authorType'] = principal.author_type.value return data def pre_validate(self, form): super(AbstractPersonLinkListField, self).pre_validate(form) for person_link in self.data: if not self.allow_authors and person_link.author_type != AuthorType.none: if not self.object_data or person_link not in self.object_data: person_link.author_type = AuthorType.none if person_link.author_type == AuthorType.none and not person_link.is_speaker: raise ValueError( _("{} has no role").format(person_link.full_name))
class fossirStaticTextField(Field): """Return an html element with text taken from this field's value""" widget = JinjaWidget('forms/static_text_widget.html') def __init__(self, *args, **kwargs): self.text_value = kwargs.pop('text', '') super(fossirStaticTextField, self).__init__(*args, **kwargs) def process_data(self, data): self.text_value = self.data = unicode(data) def _value(self): return self.text_value
class EmailRuleListField(JSONField): """A field that stores a list of e-mail template rules.""" CAN_POPULATE = True widget = JinjaWidget('events/abstracts/forms/rule_list_widget.html') accepted_condition_types = (StateCondition, TrackCondition, ContributionTypeCondition) @classproperty @classmethod def condition_class_map(cls): return {r.name: r for r in cls.accepted_condition_types} @property def condition_choices(self): return { c.name: { 'title': c.description, 'labelText': c.label_text, 'options': list(c.get_available_values(event=self.event).viewitems()), 'compatibleWith': c.compatible_with, 'required': c.required } for c in self.accepted_condition_types } def pre_validate(self, form): super(EmailRuleListField, self).pre_validate(form) if not all(self.data): raise ValueError(_('Rules may not be empty')) if any('*' in crit for rule in self.data for crit in rule.itervalues()): # '*' (any) rules should never be included in the JSON, and having # such an entry would result in the rule never passing. raise ValueError('Unexpected "*" criterion') def _value(self): return super(EmailRuleListField, self)._value() if self.data else '[]'
class EditableFileField(FileField): """A dropzone field that displays its current state and keeps track of deletes.""" widget = JinjaWidget('forms/dropzone_widget.html', editable=True) def __init__(self, *args, **kwargs): self.get_metadata = kwargs.pop('get_metadata', get_file_metadata) self.added_only = kwargs.pop('added_only', False) super(EditableFileField, self).__init__(*args, **kwargs) self.widget_options['editable'] = True def process_formdata(self, valuelist): uploaded = [] deleted = [] for value in valuelist: if isinstance(value, FileStorage): uploaded.append(value) else: deleted = json.loads(value) if not self.allow_multiple_files: uploaded = uploaded[0] if uploaded else None deleted = deleted[0] if deleted else None self.data = uploaded if self.added_only else { 'added': uploaded, 'deleted': deleted } def _value(self): # If form validation fails we still have the dict from `process_formdata` # in `self.data` which cannot be serialized so we fallback to the default # data (if there is any, i.e. if we were editing something) # It would be cleaner to still take e.g. 'deleted' into account and # save/restore the selected files with JavaScript but in most cases our # client-side validation should not fail anyway... data = self.object_data if isinstance(self.data, dict) else self.data if self.allow_multiple_files: return [self.get_metadata(f) for f in data] if data else [] else: return self.get_metadata(data) if data else None
class ContributionPersonLinkListField(PersonLinkListFieldBase): """A field to configure a list of contribution persons""" person_link_cls = ContributionPersonLink linked_object_attr = 'contrib' widget = JinjaWidget('events/contributions/forms/contribution_person_link_widget.html', allow_empty_email=True) def __init__(self, *args, **kwargs): self.author_types = AuthorType.serialize() self.allow_authors = kwargs.pop('allow_authors', kwargs['_form'].event.type == 'conference') self.allow_submitters = kwargs.pop('allow_submitters', True) self.show_empty_coauthors = kwargs.pop('show_empty_coauthors', True) self.default_author_type = kwargs.pop('default_author_type', AuthorType.none) self.default_is_submitter = kwargs.pop('default_is_submitter', True) self.default_is_speaker = True super(ContributionPersonLinkListField, self).__init__(*args, **kwargs) def _convert_data(self, data): return {self._get_person_link(x): x.pop('isSubmitter', self.default_is_submitter) for x in data} @no_autoflush def _get_person_link(self, data): extra_data = {'author_type': data.pop('authorType', self.default_author_type), 'is_speaker': data.pop('isSpeaker', self.default_is_speaker)} return super(ContributionPersonLinkListField, self)._get_person_link(data, extra_data) def _serialize_person_link(self, principal, extra_data=None): is_submitter = self.data[principal] if self.get_form().is_submitted() else None return serialize_contribution_person_link(principal, is_submitter=is_submitter) def pre_validate(self, form): super(ContributionPersonLinkListField, self).pre_validate(form) for person_link in self.data: if not self.allow_authors and person_link.author_type != AuthorType.none: if not self.object_data or person_link not in self.object_data: person_link.author_type = AuthorType.none if person_link.author_type == AuthorType.none and not person_link.is_speaker: raise ValueError(_("{} has no role").format(person_link.full_name))
class fossirWeekDayRepetitionField(Field): """ Field that lets you select an ordinal day of the week.""" widget = JinjaWidget('forms/week_day_repetition_widget.html', single_line=True) WEEK_DAY_NUMBER_CHOICES = ((1, _('first')), (2, _('second')), (3, _('third')), (4, _('fourth')), (-1, _('last'))) def __init__(self, *args, **kwargs): locale = get_current_locale() self.day_number_options = self.WEEK_DAY_NUMBER_CHOICES self.week_day_options = [(n, locale.weekday(n, short=False)) for n in xrange(7)] self.week_day_options.append((-1, _('Any day'))) self.day_number_missing = False self.week_day_missing = False super(fossirWeekDayRepetitionField, self).__init__(*args, **kwargs) def process_formdata(self, valuelist): self.data = () if any(valuelist): if not valuelist[0]: self.day_number_missing = True if not valuelist[1]: self.week_day_missing = True if valuelist: self.data = tuple(map(int, valuelist)) @property def day_number_data(self): return self.data[0] if len(self.data) > 0 else None @property def week_day_data(self): return self.data[1] if len(self.data) > 1 else None
class PaperEmailSettingsField(JSONField): CAN_POPULATE = True widget = JinjaWidget('events/papers/forms/paper_email_settings_widget.html') @property def event(self): return self.get_form().event def process_formdata(self, valuelist): if valuelist: self.data = json.loads(valuelist[0]) data = {} for key, value in self.data.iteritems(): data[key] = RoleConverter.to_python(value) if isinstance(value, list) else value self.data = data def _value(self): return { 'notify_on_added_to_event': [x.name for x in settings.get(self.event, 'notify_on_added_to_event')], 'notify_on_assigned_contrib': [x.name for x in settings.get(self.event, 'notify_on_assigned_contrib')], 'notify_on_paper_submission': [x.name for x in settings.get(self.event, 'notify_on_paper_submission')], 'notify_judge_on_review': settings.get(self.event, 'notify_judge_on_review'), 'notify_author_on_judgment': settings.get(self.event, 'notify_author_on_judgment') }
class OverrideMultipleItemsField(HiddenField): """A field similar to `MultipleItemsField` which allows the user to override some values. :param fields: a list of ``(fieldname, title)`` tuples. Should match the fields of the corresponding `MultipleItemsField`. :param field_data: the data from the corresponding `MultipleItemsField`. :param unique_field: the name of the field which is unique among all rows :param edit_fields: a set containing the field names which can be edited If you decide to use this field, please consider adding support for `uuid_field` here! """ widget = JinjaWidget('forms/override_multiple_items_widget.html') def __init__(self, *args, **kwargs): self.fields = kwargs.pop('fields') self.field_data = kwargs.pop( 'field_data', None) # usually set after creating the form instance self.unique_field = kwargs.pop('unique_field') self.edit_fields = set(kwargs.pop('edit_fields')) super(OverrideMultipleItemsField, self).__init__(*args, **kwargs) def process_formdata(self, valuelist): if is_preprocessed_formdata(valuelist): self.data = valuelist[0] elif valuelist: self.data = json.loads(valuelist[0]) def pre_validate(self, form): valid_keys = {x[self.unique_field] for x in self.field_data} for key, values in self.data.items(): if key not in valid_keys: # e.g. a row removed from field_data that had a value before del self.data[key] continue if set(values.viewkeys()) > self.edit_fields: # e.g. a field that was editable before self.data[key] = { k: v for k, v in values.iteritems() if k in self.edit_fields } # Remove anything empty for key, values in self.data.items(): for field, value in values.items(): if not value: del values[field] if not self.data[key]: del self.data[key] def _value(self): return self.data or {} def get_overridden_value(self, row, name): """Utility for the widget to get the entered value for an editable field""" key = self.get_row_key(row) return self._value().get(key, {}).get(name, '') def get_row_key(self, row): """Utility for the widget to get the unique value for a row""" return row[self.unique_field]
class PrincipalListField(HiddenField): """A field that lets you select a list fossir user/group ("principal") :param groups: If groups should be selectable. :param allow_networks: If ip networks should be selectable. :param allow_emails: If emails should be allowed. :param allow_external: If "search users with no fossir account" should be available. Selecting such a user will automatically create a pending user once the form is submitted, even if other fields in the form fail to validate! """ widget = JinjaWidget('forms/principal_list_widget.html', single_kwargs=True) def __init__(self, *args, **kwargs): self.allow_emails = kwargs.pop('allow_emails', False) self.groups = kwargs.pop('groups', False) self.allow_networks = kwargs.pop('allow_networks', False) self.ip_networks = [] if self.allow_networks: self.ip_networks = map( serialize_ip_network_group, IPNetworkGroup.query.filter_by(hidden=False)) # Whether it is allowed to search for external users with no fossir account self.allow_external = kwargs.pop('allow_external', False) # Whether the add user dialog is opened immediately when the field is displayed self.open_immediately = kwargs.pop('open_immediately', False) super(PrincipalListField, self).__init__(*args, **kwargs) def _convert_principal(self, principal): return principal_from_fossil(principal, allow_pending=self.allow_external, allow_emails=self.allow_emails, allow_networks=self.allow_networks, existing_data=self.object_data) def process_formdata(self, valuelist): if valuelist: self.data = { self._convert_principal(x) for x in json.loads(valuelist[0]) } def pre_validate(self, form): if not self.groups and any( isinstance(p, GroupProxy) for p in self._get_data()): raise ValueError('You cannot select groups') def _serialize_principal(self, principal): if principal.principal_type == PrincipalType.email: return principal.fossilize() elif principal.principal_type == PrincipalType.network: return serialize_ip_network_group(principal) elif principal.principal_type == PrincipalType.user: return serialize_user(principal) elif principal.is_group: return serialize_group(principal) else: raise ValueError('Invalid principal: {} ({})'.format( principal, principal.principal_type)) def _value(self): from fossir.modules.events.models.persons import PersonLinkBase def key(obj): if isinstance(obj, PersonLinkBase): return obj.name.lower() return obj.principal_type, obj.name.lower() principals = sorted(self._get_data(), key=key) return map(self._serialize_principal, principals) def _get_data(self): return sorted(self.data) if self.data else []
class TimeDeltaField(Field): """A field that lets the user select a simple timedelta. It does not support mixing multiple units, but it is smart enough to switch to a different unit to represent a timedelta that could not be represented otherwise. :param units: The available units. Must be a tuple containing any any of 'seconds', 'minutes', 'hours' and 'days'. If not specified, ``('hours', 'days')`` is assumed. """ widget = JinjaWidget('forms/timedelta_widget.html', single_line=True, single_kwargs=True) # XXX: do not translate, "Minutes" is ambiguous without context unit_names = { 'seconds': 'Seconds', 'minutes': 'Minutes', 'hours': 'Hours', 'days': 'Days' } magnitudes = OrderedDict([('days', 86400), ('hours', 3600), ('minutes', 60), ('seconds', 1)]) def __init__(self, *args, **kwargs): self.units = kwargs.pop('units', ('hours', 'days')) super(TimeDeltaField, self).__init__(*args, **kwargs) @property def best_unit(self): """Return the largest unit that covers the current timedelta""" if self.data is None: return None seconds = int(self.data.total_seconds()) for unit, magnitude in self.magnitudes.iteritems(): if not seconds % magnitude: return unit return 'seconds' @property def choices(self): best_unit = self.best_unit choices = [(unit, self.unit_names[unit]) for unit in self.units] # Add whatever unit is necessary to represent the currenet value if we have one if best_unit and best_unit not in self.units: choices.append( (best_unit, '({})'.format(self.unit_names[best_unit]))) return choices def process_formdata(self, valuelist): if valuelist and len(valuelist) == 2: value = int(valuelist[0]) unit = valuelist[1] if unit not in self.magnitudes: raise ValueError('Invalid unit') self.data = timedelta(seconds=self.magnitudes[unit] * value) def pre_validate(self, form): if self.best_unit in self.units: return if self.object_data is None: raise ValueError(_('Please choose a valid unit.')) if self.object_data != self.data: raise ValueError( _('Please choose a different unit or keep the previous value.') ) def _value(self): if self.data is None: return '', '' else: return int(self.data.total_seconds()) // self.magnitudes[ self.best_unit], self.best_unit
class RelativeDeltaField(Field): """A field that lets the user select a simple timedelta. It does not support mixing multiple units, but it is smart enough to switch to a different unit to represent a timedelta that could not be represented otherwise. :param units: The available units. Must be a tuple containing any any of 'seconds', 'minutes', 'hours' and 'days'. If not specified, ``('hours', 'days')`` is assumed. """ widget = JinjaWidget('forms/timedelta_widget.html', single_line=True, single_kwargs=True) # XXX: do not translate, "Minutes" is ambiguous without context unit_names = { 'seconds': 'Seconds', 'minutes': 'Minutes', 'hours': 'Hours', 'days': 'Days', 'weeks': 'Weeks', 'months': 'Months', 'years': 'Years' } magnitudes = OrderedDict([('years', relativedelta(years=1)), ('months', relativedelta(months=1)), ('weeks', relativedelta(weeks=1)), ('days', relativedelta(days=1)), ('hours', relativedelta(hours=1)), ('minutes', relativedelta(minutes=1)), ('seconds', relativedelta(seconds=1))]) def __init__(self, *args, **kwargs): self.units = kwargs.pop('units', ('hours', 'days')) super(RelativeDeltaField, self).__init__(*args, **kwargs) @property def split_data(self): if self.data is None: return None, None for unit in self.magnitudes: number = getattr(self.data, unit) if number: return number, unit raise ValueError('Unsupported relativedelta() unit') @property def choices(self): choices = [(unit, self.unit_names[unit]) for unit in self.units] number, unit = self.split_data if number is not None and unit not in self.units: # Add whatever unit is necessary to represent the currenet value if we have one choices.append((unit, '({})'.format(self.unit_names[unit]))) return choices def process_formdata(self, valuelist): if valuelist and len(valuelist) == 2: value = int(valuelist[0]) unit = valuelist[1] if unit not in self.magnitudes: raise ValueError('Invalid unit') self.data = (self.magnitudes[unit] * value).normalized() def pre_validate(self, form): if self.object_data is None: raise ValueError(_('Please choose a valid unit.')) def _value(self): if self.data is None: return '', '' return self.split_data
class CategoryField(HiddenField): """WTForms field that lets you select a category. :param allow_events: Whether to allow selecting a category that contains events. :param allow_subcats: Whether to allow selecting a category that contains subcategories. :param require_event_creation_rights: Whether to allow selecting only categories where the user can create events. """ widget = JinjaWidget('forms/category_picker_widget.html') def __init__(self, *args, **kwargs): self.navigator_category_id = 0 self.allow_events = kwargs.pop('allow_events', True) self.allow_subcats = kwargs.pop('allow_subcats', True) self.require_event_creation_rights = kwargs.pop( 'require_event_creation_rights', False) super(CategoryField, self).__init__(*args, **kwargs) def pre_validate(self, form): if self.data: self._validate(self.data) def process_data(self, value): if not value: self.data = None return try: self._validate(value) except ValueError: self.data = None self.navigator_category_id = value.id else: self.data = value self.navigator_category_id = value.id def process_formdata(self, valuelist): from fossir.modules.categories import Category if valuelist: try: category_id = int(json.loads(valuelist[0])['id']) except KeyError: self.data = None else: self.data = Category.get(category_id, is_deleted=False) def _validate(self, category): if not self.allow_events and category.has_only_events: raise ValueError( _("Categories containing only events are not allowed.")) if not self.allow_subcats and category.children: raise ValueError( _("Categories containing subcategories are not allowed.")) def _value(self): return { 'id': self.data.id, 'title': self.data.title } if self.data else {} def _get_data(self): return self.data
class fossirRadioField(RadioField): widget = JinjaWidget('forms/radio_buttons_widget.html', single_kwargs=True) def __init__(self, *args, **kwargs): self.option_orientation = kwargs.pop('orientation', 'vertical') super(fossirRadioField, self).__init__(*args, **kwargs)
class fossirTagListField(HiddenFieldList): widget = JinjaWidget('forms/tag_list_widget.html', single_kwargs=True)
class MultiStringField(HiddenField): """A field with multiple input text fields. :param field: A tuple ``(fieldname, title)`` where the title is used in the placeholder. :param uuid_field: If set, each item will have a UUID assigned and stored in the field specified here. :param flat: If True, the field returns a list of string values instead of dicts. Cannot be combined with `uuid_field`. :param unique: Whether the values should be unique. :param sortable: Whether items should be sortable. """ widget = JinjaWidget('forms/multiple_text_input_widget.html', single_line=True) def __init__(self, *args, **kwargs): self.field_name, self.field_caption = kwargs.pop('field') self.sortable = kwargs.pop('sortable', False) self.unique = kwargs.pop('unique', False) self.flat = kwargs.pop('flat', False) self.uuid_field = kwargs.pop('uuid_field', None) if self.flat and self.uuid_field: raise ValueError('`uuid_field` and `flat` are mutually exclusive') super(MultiStringField, self).__init__(*args, **kwargs) def process_formdata(self, valuelist): if is_preprocessed_formdata(valuelist): self.data = valuelist[0] elif valuelist: self.data = json.loads(valuelist[0]) if self.uuid_field: for item in self.data: if self.uuid_field not in item: item[self.uuid_field] = unicode(uuid.uuid4()) def pre_validate(self, form): try: if not all(isinstance(item, dict) for item in self.data): raise ValueError('Invalid data. Expected list of dicts.') if self.unique: unique_values = {item[self.field_name] for item in self.data} if len(unique_values) != len(self.data): raise ValueError('Items must be unique') if self.uuid_field: unique_uuids = { uuid.UUID(item[self.uuid_field], version=4) for item in self.data } if len(unique_uuids) != len(self.data): raise ValueError('UUIDs must be unique') if not all(item[self.field_name].strip() for item in self.data): raise ValueError('Empty items are not allowed') finally: if self.flat: self.data = [x[self.field_name] for x in self.data] def _value(self): if not self.data: return [] elif self.flat: return [{self.field_name: x} for x in self.data] else: return self.data
class fossirQuerySelectMultipleCheckboxField(fossirQuerySelectMultipleField): option_widget = CheckboxInput() widget = JinjaWidget('forms/checkbox_group_widget.html', single_kwargs=True)
class fossirEnumRadioField(fossirEnumSelectField): widget = JinjaWidget('forms/radio_buttons_widget.html', orientation='horizontal', single_kwargs=True) option_widget = RadioInput()
class MultipleItemsField(HiddenField): """A field with multiple items consisting of multiple string values. :param fields: A list of dicts with the following arguments: 'id': the unique ID of the field 'caption': the title of the column and the placeholder 'type': 'text|number|select', the type of the field 'coerce': callable to convert the value to a python type. the type must be comvertible back to a string, so usually you just want something like `int` or `float` here. In case the type is 'select', the property 'choices' of the `MultipleItemsField` or the 'choices' kwarg needs to be a dict where the key is the 'id' of the select field and the value is another dict mapping the option's id to it caption. :param uuid_field: If set, each item will have a UUID assigned and stored in the field specified here. The name specified here may not be in `fields`. :param uuid_field_opaque: If set, the `uuid_field` is considered opaque, i.e. it is never touched by this field. This is useful when you subclass the field and use e.g. actual database IDs instead of UUIDs. :param unique_field: The name of a field in `fields` that needs to be unique. :param sortable: Whether items should be sortable. """ widget = JinjaWidget('forms/multiple_items_widget.html') def __init__(self, *args, **kwargs): self.fields = getattr(self, 'fields', None) or kwargs.pop('fields') self.uuid_field = kwargs.pop('uuid_field', None) self.uuid_field_opaque = kwargs.pop('uuid_field_opaque', False) self.unique_field = kwargs.pop('unique_field', None) self.sortable = kwargs.pop('sortable', False) self.choices = getattr(self, 'choices', kwargs.pop('choices', {})) self.serialized_data = {} if self.uuid_field: assert self.uuid_field != self.unique_field assert self.uuid_field not in self.fields self.field_names = { item['id']: item['caption'] for item in self.fields } super(MultipleItemsField, self).__init__(*args, **kwargs) def process_formdata(self, valuelist): if is_preprocessed_formdata(valuelist): self.data = valuelist[0] if valuelist: self.data = json.loads(valuelist[0]) # Preserve dict data, because the self.data can be modified by a subclass self.serialized_data = json.loads(valuelist[0]) if self.uuid_field and not self.uuid_field_opaque: for item in self.data: if self.uuid_field not in item: item[self.uuid_field] = unicode(uuid.uuid4()) def pre_validate(self, form): unique_used = set() uuid_used = set() coercions = { f['id']: f['coerce'] for f in self.fields if f.get('coerce') is not None } for i, item in enumerate(self.serialized_data): if not isinstance(item, dict): raise ValueError('Invalid item type: {}'.format( type(item).__name__)) item_keys = set(item) if self.uuid_field: item_keys.discard(self.uuid_field) if item_keys != {x['id'] for x in self.fields}: raise ValueError('Invalid item (bad keys): {}'.format( escape(', '.join(item.viewkeys())))) if self.unique_field: if item[self.unique_field] in unique_used: raise ValueError('{} must be unique'.format( self.field_names[self.unique_field])) unique_used.add(item[self.unique_field]) if self.uuid_field and not self.uuid_field_opaque: if item[self.uuid_field] in uuid_used: raise ValueError('UUID must be unique') # raises ValueError if uuid is invalid uuid.UUID(item[self.uuid_field], version=4) uuid_used.add(item[self.uuid_field]) for key, fn in coercions.viewitems(): try: self.data[i][key] = fn(self.data[i][key]) except ValueError: raise ValueError( u"Invalid value for field '{}': {}".format( self.field_names[key], escape(item[key]))) def _value(self): return self.data or [] @property def _field_spec(self): # Field data for the widget; skip non-json-serializable data return [{k: v for k, v in field.iteritems() if k != 'coerce'} for field in self.fields]