class IImageButton(IButton): """An image button in a form.""" image = TextLine( title=_('Image Path'), description=_('A relative image path to the root of the resources'), required=True)
class IFieldsForm(IForm): """A form that is based upon defined fields.""" fields = Object(title=_('Fields'), description=_( 'A field manager describing the fields to be used for ' 'the form.'), schema=IFields)
class IHandlerForm(Interface): """A form that stores the handlers locally.""" handlers = Object( title=_('Handlers'), description=_('A list of action handlers defined on the form.'), schema=IButtonHandlers, required=True)
class IActionEvent(Interface): """An event specific for an action.""" action = Object( title=_('Action'), description=_('The action for which the event is created.'), schema=IAction, required=True)
class IButtonForm(IForm): """A form that is based upon defined buttons.""" buttons = Object( title=_('Buttons'), description=_('A button manager describing the buttons to be used for ' 'the form.'), schema=IButtons)
class IFieldsAndContentProvidersForm(IForm): """A form that is based upon defined fields and content providers""" content_providers = Object( title=_('Content providers'), description=_( 'A manager describing the content providers to be used for ' 'the form.'), schema=IContentProviders)
class EditForm(Form): """A simple edit form with an apply button.""" success_message = _('Data successfully updated.') no_changes_message = _('No changes were applied.') @button_and_handler(_('Apply'), name='apply') def handle_apply(self, action): # pylint: disable=unused-argument """Apply action handler""" data, errors = {}, {} for form in self.get_forms(): form_data, form_errors = form.extract_data() if form_errors: if not IGroup.providedBy(form): form.status = getattr(form, 'form_errors_message', self.form_errors_message) errors[form] = form_errors data[form] = form_data if errors: self.status = self.form_errors_message return changes = self.apply_changes(data) if changes: self.status = self.success_message else: self.status = self.no_changes_message self.finished_state.update({'action': action, 'changes': changes}) def apply_changes(self, data): """Apply updates to form context""" changes = {} contents, changed_contents = {}, {} for form in self.get_forms(): if form.mode == DISPLAY_MODE: continue content = form.get_content() form_changes = apply_changes(form, content, data) if form_changes: merge_changes(changes, form_changes) content_hash = ICacheKeyValue(content) contents[content_hash] = content merge_changes(changed_contents.setdefault(content_hash, {}), form_changes) if changes: # Construct change-descriptions for the object-modified event for content_hash, content_changes in changed_contents.items(): descriptions = [] for interface, names in content_changes.items(): descriptions.append(Attributes(interface, *names)) # Send out a detailed object-modified event self.request.registry.notify( ObjectModifiedEvent(contents[content_hash], *descriptions)) return changes
class IBoolTerms(ITerms): """A specialization that handles boolean choices.""" true_label = TextLine( title=_('True-value Label'), description=_('The label for a true value of the Bool field'), required=True) false_label = TextLine( title=_('False-value Label'), description=_('The label for a false value of the Bool field'), required=False)
class MultiWidget(HTMLFormElement, MultiWidgetBase, FormMixin): # pylint: disable=function-redefined """Multi widget implementation.""" buttons = Buttons() prefix = 'widget' klass = 'multi-widget' css = 'multi' items = () actions = None show_label = True # show labels for item subwidgets or not # Internal attributes _adapter_value_attributes = MultiWidgetBase._adapter_value_attributes + ( 'show_label', ) def update(self): """See pyams_form.interfaces.widget.IWidget.""" super().update() self.update_actions() self.actions.execute() self.update_actions() # Update again, as conditions may change def update_actions(self): """Update widget actions""" self.update_allow_add_remove() if self.name is not None: self.prefix = self.name registry = self.request.registry self.actions = registry.getMultiAdapter((self, self.request, self), IActions) self.actions.update() @button_and_handler(_('Add'), name='add', condition=attrgetter('allow_adding')) def handle_add(self, action): # pylint: disable=unused-argument """Add button handler""" self.append_adding_widget() @button_and_handler(_('Remove selected'), name='remove', condition=attrgetter('allow_removing')) def handle_remove(self, action): # pylint: disable=unused-argument """Remove button handler""" self.remove_widgets([ widget.name for widget in self.widgets if '{}.remove'.format(widget.name) in self.request.params ])
class IActionForm(Interface): """A form that stores executable actions""" actions = Object(title=_('Actions'), description=_('A list of actions defined on the form'), schema=IActions, required=True) refresh_actions = Bool(title=_('Refresh actions'), description=_( 'A flag, when set, causes form actions to be ' 'updated again after their execution.'), default=False, required=True)
class IAction(Interface): """Action""" __name__ = TextLine(title=_('Name'), description=_('The object name.'), required=False, default=None) title = TextLine(title=_('Title'), description=_('The action title.'), required=True) def is_executed(self): """Determine whether the action has been executed."""
class IButton(IField): """A button in a form.""" access_key = TextLine( title=_('Access Key'), description=_('The key when pressed causes the button to be pressed'), min_length=1, max_length=1, required=False) action_factory = Field(title=_('Action Factory'), description=_('The action factory'), required=False, default=None, missing_value=None)
class AJAXAddForm(AJAXForm): """AJAX add form mix-in class""" no_changes_message = _("No data was created.") def get_ajax_output(self, changes): request = self.request # pylint: disable=no-member # pylint: disable=no-member renderer = None if 'action' in self.finished_state: name = self.finished_state['action'].field.getName() renderer = queryMultiAdapter((self.context, request, self), IAJAXFormRenderer, name=name) if renderer is None: renderer = queryMultiAdapter((self.context, request, self), IAJAXFormRenderer) if renderer is not None: result = renderer.render(changes) if result: return result if changes is None: return { 'status': 'info', 'message': request.localizer.translate(self.no_changes_message) } return {'status': 'reload'}
def extract_file_name(form, widget_id, cleanup=True, allow_empty_postfix=False): """Extract the filename of the widget with the given id. Uploads from win/IE need some cleanup because the filename includes also the path. The option ``cleanup=True`` will do this for you. The option ``allowEmptyPostfix`` allows to have a filename without extensions. By default this option is set to ``False`` and will raise a ``ValueError`` if a filename doesn't contain a extension. """ widget = get_widget_by_id(form, widget_id) clean_file_name = '' dotted_parts = [] if not allow_empty_postfix or cleanup: # We need to strip out the path section even if we do not reomve them # later, because we just need to check the filename extension. clean_file_name = widget.filename.split('\\')[-1] clean_file_name = clean_file_name.split('/')[-1] dotted_parts = clean_file_name.split('.') if not allow_empty_postfix: if len(dotted_parts) <= 1: raise ValueError(_('Missing filename extension.')) if cleanup: return clean_file_name return widget.filename
class IErrorViewSnippet(Interface): """A view providing a view for an error""" widget = Field(title=_("Widget"), description=_("The widget that the view is on"), required=True) error = Field(title=_('Error'), description=_('Error the view is for'), required=True) def update(self): """Update view""" def render(self): """Render view"""
class ValueErrorViewSnippet(ErrorViewSnippet): """An error view for ValueError.""" default_message = _('The system could not process the given value.') def create_message(self): return self.default_message
def _make_missing_term(self, value): """Return a term that should be displayed for the missing token""" uvalue = to_unicode(value) return SimpleTerm(value, self._make_token(value), title=_('Missing: ${value}', mapping=dict(value=uvalue)))
class IData(Interface): """A proxy object for form data. The object will make all keys within its data attribute available as attributes. The schema that is represented by the data will be directly provided by instances. """ def __init__(self, schema, data, context): # pylint: disable=super-init-not-called """The data proxy is instantiated using the schema it represents, the data fulfilling the schema and the context in which the data are validated. """ __context__ = Field( title=_('Context'), description=_('The context in which the data are validated'), required=True)
class IContextAware(Interface): """Offers a context attribute. For advanced uses, the widget will make decisions based on the context it is rendered in. """ context = Field( title=_('Context'), description=_('The context in which the widget is displayed.'), required=True) ignore_context = Bool( title=_('Ignore Context'), description=_('A flag, when set, forces the widget not to look at ' 'the context for a value.'), default=False, required=False)
class BoolTerms(Terms): """Default yes and no terms are used by default for IBool fields.""" true_label = _('yes') false_label = _('no') def __init__(self, context, request, form, field, widget): # pylint: disable=too-many-arguments self.context = context self.request = request self.form = form self.field = field self.widget = widget terms = [ SimpleTerm(*args) for args in [(True, 'true', self.true_label), (False, 'false', self.false_label)] ] self.terms = SimpleVocabulary(terms)
class EditSubForm(BaseForm): """Edit sub-form""" form_errors_message = _('There were some errors.') success_message = _('Data successfully updated.') no_changes_message = _('No changes were applied.') def __init__(self, context, request, parent_form): super().__init__(context, request) self.parent_form = self.__parent__ = parent_form @handler(EditForm.buttons['apply']) def handle_apply(self, action): # pylint: disable=unused-argument """Handler for apply button""" data, errors = self.widgets.extract() if errors: self.status = self.form_errors_message return content = self.get_content() changed = apply_changes(self, content, data) if changed: registry = self.request.registry registry.notify(ObjectModifiedEvent(content)) self.status = self.success_message else: self.status = self.no_changes_message def update(self): super().update() registry = self.request.registry for action in self.parent_form.actions.executed_actions: adapter = registry.queryMultiAdapter( (self, self.request, self.get_content(), action), IActionHandler) if adapter: adapter()
def to_field_value(self, value): """See interfaces.IDataConverter""" if value is None or value == '': # When no new file is uploaded, send a signal that we do not want # to do anything special. return NOT_CHANGED # By default a IBytes field is used for get a file upload widget. # But interfaces extending IBytes do not use file upload widgets. # Any way if we get a FieldStorage of FileUpload object, we'll # convert it. # We also store the additional FieldStorage/FileUpload values on the widget # before we loose them. if isinstance(value, (FieldStorage, FileUpload)): self.widget.headers = value.headers self.widget.filename = value.filename try: if isinstance(value, FieldStorage): if value.fp is None: seek = value.file.seek read = value.file.read else: seek = value.fp.seek read = value.fp.read else: seek = value.seek read = value.read except AttributeError as e: # pylint: disable=invalid-name raise ValueError(_('Bytes data are not a file object')) from e else: seek(0) data = read() if data or getattr(value, 'filename', ''): return data return self.field.missing_value else: return to_bytes(value)
class Form(BaseForm): """The Form.""" buttons = Buttons() method = FieldProperty(IInputForm['method']) enctype = FieldProperty(IInputForm['enctype']) accept_charset = FieldProperty(IInputForm['accept_charset']) accept = FieldProperty(IInputForm['accept']) autocomplete = FieldProperty(IInputForm['autocomplete']) actions = FieldProperty(IActionForm['actions']) refresh_actions = FieldProperty(IActionForm['refresh_actions']) # AJAX related form properties ajax_form_handler = FieldProperty(IInputForm['ajax_form_handler']) ajax_form_options = FieldProperty(IInputForm['ajax_form_options']) ajax_form_target = FieldProperty(IInputForm['ajax_form_target']) ajax_form_callback = FieldProperty(IInputForm['ajax_form_callback']) # common string for use in validation status messages form_errors_message = _('There were some errors.') def __init__(self, context, request): super().__init__(context, request) self.finished_state = {} @property def action(self): """See interfaces.IInputForm""" return self.request.url @property def name(self): """See interfaces.IInputForm""" return self.prefix.strip('.') @property def id(self): # pylint: disable=invalid-name """Form ID""" return self.name.replace('.', '-') def update_actions(self): """Update form actions""" registry = self.request.registry self.actions = registry.getMultiAdapter( (self, self.request, self.get_content()), IActions) self.actions.update() def update(self): super().update() self.update_actions() self.actions.execute() if self.refresh_actions: self.update_actions() def get_ajax_handler(self): """Get absolute URL of AJAX handler""" return absolute_url(self.context, self.request, self.ajax_form_handler) def get_form_options(self): """Get form options in JSON format""" return json.dumps( self.ajax_form_options) if self.ajax_form_options else None
class IField(Interface): """Field wrapping a schema field used in the form.""" __name__ = TextLine(title=_('Title'), description=_('The name of the field within the form'), required=True) field = Field(title=_('Schema Field'), description=_('The schema field that is to be rendered'), required=True) prefix = Field( title=_('Prefix'), description=_('The prefix of the field used to avoid name clashes'), required=True) mode = Field( title=_('Mode'), description=_('The mode in which to render the widget for the field'), required=True) interface = Field( title=_('Interface'), description=_('The interface from which the field is coming'), required=True) ignore_context = Bool( title=_('Ignore Context'), description=_('A flag, when set, forces the widget not to look at ' 'the context for a value'), required=False) widget_factory = Field(title=_('Widget Factory'), description=_('The widget factory'), required=False, default=None, missing_value=None) show_default = Bool(title=_('Show default value'), description=_( 'A flag, when set, makes the widget to display ' 'field|adapter provided default values'), default=True, required=False)
class AddForm(Form): """A field and button based add form.""" ignore_context = True ignore_readonly = True content_factory = None @button_and_handler(_('Add'), name='add') def handle_add(self, action): # pylint: disable=unused-argument """Handler for *add* button""" data, errors = {}, {} for form in self.get_forms(): form_data, form_errors = form.extract_data() if form_errors: if not IGroup.providedBy(form): form.status = getattr(form, 'form_errors_message', self.form_errors_message) errors[form] = form_errors data[form] = form_data if errors: self.status = self.form_errors_message return obj = self.create_and_add(data) if obj is not None: # mark only as finished if we get the new object self.finished_state.update({'action': action, 'changes': obj}) def create_and_add(self, data): """Create new content and add it to context""" obj = self.create(data.get(self, {})) self.request.registry.notify(ObjectCreatedEvent(obj)) if IPersistent.providedBy( obj): # temporary locate to fix raising of INotYet exceptions locate(obj, self.context) self.update_content(obj, data) self.add(obj) return obj def create(self, data): # pylint: disable=unused-argument """Create new content from form data""" if self.content_factory is not None: factory = get_object_factory(self.content_factory) \ if is_interface(self.content_factory) else self.content_factory return factory() # pylint: disable=not-callable raise NotImplementedError def add(self, obj): # pylint: disable=redefined-builtin """Add new object to form context""" raise NotImplementedError def update_content(self, obj, data): """Update content with form data after creation""" changes = {} for form in self.get_forms(): if form.mode == DISPLAY_MODE: continue merge_changes(changes, apply_changes(form, obj, data)) return changes def next_url(self): """Redirection URL after object creation""" return self.action def render(self): if self.finished_state: self.request.response.location = self.next_url() self.request.response.status = 302 return '' return super().render()
class SelectWidget(HTMLSelectWidget, SequenceWidget): """Select widget implementation.""" klass = 'select-widget' css = 'select' prompt = False no_value_message = _('No value') prompt_message = _('Select a value...') # Internal attributes _adapter_value_attributes = SequenceWidget._adapter_value_attributes + \ ('no_value_message', 'prompt_message', 'prompt') def is_selected(self, term): """Check for term selection""" return term.token in self.value def update(self): """See pyams_form.interfaces.widget.IWidget.""" super().update() add_field_class(self) @property def items(self): """Items list getter""" if self.terms is None: # update() has not been called yet return () items = [] if (not self.required or self.prompt) and self.multiple is None: if self.prompt: message = self.prompt_message else: message = self.no_value_message items.append({ 'id': self.id + '-novalue', 'value': self.no_value_token, 'content': message, 'selected': self.value in ((), []) }) ignored = set(self.value) def add_item(idx, term, prefix=''): selected = self.is_selected(term) if selected and term.token in ignored: ignored.remove(term.token) item_id = '%s-%s%i' % (self.id, prefix, idx) content = term.token if ITitledTokenizedTerm.providedBy(term): content = self.request.localizer.translate(term.title) items.append({ 'id': item_id, 'value': term.token, 'content': content, 'selected': selected }) for idx, term in enumerate(self.terms): add_item(idx, term) if ignored: # some values are not displayed, probably they went away from the vocabulary for idx, token in enumerate(sorted(ignored)): try: term = self.terms.getTermByToken(token) except LookupError: # just in case the term really went away continue add_item(idx, term, prefix='missing-') return items def json_data(self): data = super().json_data() data['type'] = 'select' data['options'] = self.items return data
class DecimalDataConverter(NumberDataConverter): """A data converter for integers.""" type = decimal.Decimal error_message = _('The entered value is not a valid decimal literal.')
class IntegerDataConverter(NumberDataConverter): """A data converter for integers.""" type = int error_message = _('The entered value is not a valid integer literal.')
class IForm(Interface): """Form interface""" mode = Choice(title=_('Mode'), description=_('The mode in which to render the widgets.'), values=(INPUT_MODE, DISPLAY_MODE), required=True) ignore_context = Bool( title=_('Ignore Context'), description=_('If set the context is ignored to retrieve a value.'), default=False, required=True) ignore_request = Bool( title=_('Ignore Request'), description=_('If set the request is ignored to retrieve a value.'), default=False, required=True) ignore_readonly = Bool( title=_('Ignore Readonly'), description=_('If set then readonly fields will also be shown.'), default=False, required=True) ignore_required_on_extract = Bool( title=_('Ignore Required validation on extract'), description=_( "If set then required fields will pass validation " "on extract regardless whether they're filled in or not"), default=False, required=True) widgets = Object( title=_('Widgets'), description=_('A widget manager containing the widgets to be used in ' 'the form.'), schema=IWidgets) title = TextLine(title=_('Title'), description=_('Main form title'), required=False) legend = TextLine( title=_('Legend'), description=_('A human readable text describing the form that can be ' 'used in the UI.'), required=False) required_label = TextLine( title=_('Required label'), description=_('A human readable text describing the form that can ' 'be used in the UI for rendering a required info ' 'legend.'), required=False) prefix = ASCIILine( title=_('Prefix'), description=_('The prefix of the form used to uniquely identify it.'), default='form.') status = Text(title=_('Status'), description=_('The status message of the form.'), default=None, required=False) def get_content(self): """Return the content to be displayed and/or edited.""" def update_widgets(self, prefix=None): """Update the widgets for the form. This method is commonly called from the ``update()`` method and is mainly meant to be a hook for subclasses. Note that you can pass an argument for ``prefix`` to override the default value of ``"widgets."``. """ def extract_data(self, set_errors=True): """Extract the data of the form. set_errors: needs to be passed to extract() and to sub-widgets""" def update(self): """Update the form.""" def render(self): """Render the form.""" def json(self): """Returns the form in json format"""
class IInputForm(Interface): """A form that is meant to process the input of the form controls.""" action = URI(title=_('Action'), description=_( 'The action defines the URI to which the form data are ' 'sent.'), required=True) name = TextLine(title=_('Name'), description=_('The name of the form used to identify it.'), required=False) # pylint: disable=invalid-name id = TextLine(title=_('Id'), description=_('The id of the form used to identify it.'), required=False) method = Choice(title=_('Method'), description=_('The HTTP method used to submit the form.'), values=('get', 'post'), default='post', required=False) enctype = ASCIILine( title=_('Encoding Type'), description=_('The data encoding used to submit the data safely.'), default='multipart/form-data', required=False) accept_charset = ASCIILine( title=_('Accepted Character Sets'), description=_('This is a list of character sets the server ' 'accepts. By default this is unknown.'), required=False) accept = ASCIILine(title=_('Accepted Content Types'), description=_( 'This is a list of content types the server can ' 'safely handle.'), required=False) autocomplete = Choice( title=_("Form autocomplete"), description=_("Enable or disable global form autocomplete"), values=('on', 'off', 'new-password'), required=False) # AJAX related form settings ajax_form_handler = TextLine(title="Name of AJAX form handler", required=False) ajax_form_options = Dict(title="AJAX form submit's data options", required=False) ajax_form_target = TextLine( title="Form submit target", description="Form content target, used for HTML and text content " "types", required=False) ajax_form_callback = TextLine( title="AJAX submit callback", description="Name of a custom form submit callback", required=False) def get_ajax_handler(self): """Get absolute URL of AJAX handler""" def get_form_options(self): """Get form options in JSON format"""