# HG changeset patch # User Thierry Florac # Date 1575453925 -3600 # Node ID c435de184bdaca3fd3643234f9a7900e39afda88 # Parent ba372884335fce114ddab5bc2c2181ba68f6e62e Code cleanup diff -r ba372884335f -r c435de184bda src/pyams_form/__init__.py --- a/src/pyams_form/__init__.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/__init__.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,17 +10,23 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form package + +This package is an extension to z3c.form. + +It allows to integrate z3c forms into Pyramid; it is adding some features like inner subforms +(handled with adapters), form groups (which are ued to group form fields together inside a +fieldset in a form), modal forms (which are displayed into a modal window), and is providing +default templates for all these elements to be displayed correctly when using PyAMS_zmi package +or any other Bootstrap-based skin. +""" + +from pyramid.i18n import TranslationStringFactory -# import standard library - -# import interfaces +_ = TranslationStringFactory('pyams_form') -# import packages - -from pyramid.i18n import TranslationStringFactory -_ = TranslationStringFactory('pyams_form') +__docformat__ = 'restructuredtext' def includeme(config): @@ -28,5 +34,5 @@ Split in another package to remove cyclic dependencies with TranslationStringFactory """ - from .include import include_package + from .include import include_package # pylint: disable=import-outside-toplevel include_package(config) diff -r ba372884335f -r c435de184bda src/pyams_form/doctests/README.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_form/doctests/README.rst Wed Dec 04 11:05:25 2019 +0100 @@ -0,0 +1,11 @@ +================== +pyams_form package +================== + + >>> from pyramid.testing import setUp, tearDown + >>> config = setUp() + + >>> from pyams_form.interfaces.form import * + >>> from pyams_form.form import * + + >>> tearDown() diff -r ba372884335f -r c435de184bda src/pyams_form/doctests/README.txt --- a/src/pyams_form/doctests/README.txt Tue Nov 19 16:30:58 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -================== -pyams_ package -================== diff -r ba372884335f -r c435de184bda src/pyams_form/form.py --- a/src/pyams_form/form.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/form.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,12 +10,15 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.form module -import logging -logger = logging.getLogger('PyAMS (form)') +This module is the core of PyAMS_form package; it provides additions to form management as +provided by z3c.form package. +""" import json +import logging + import transaction import venusian from persistent import IPersistent @@ -25,11 +28,11 @@ from pyramid_chameleon.interfaces import IChameleonTranslate from pyramid_zope_request import PyramidPublisherRequest, PyramidToPublisher from z3c.form.button import Buttons -from z3c.form.form import AddForm as BaseAddForm, DisplayForm as BaseDisplayForm, EditForm as BaseEditForm, Form, \ - applyChanges +from z3c.form.form import AddForm as BaseAddForm, DisplayForm as BaseDisplayForm, \ + EditForm as BaseEditForm, Form, applyChanges from z3c.form.interfaces import DISPLAY_MODE, IErrorViewSnippet, IMultipleErrors from zope.component import queryUtility -from zope.interface import Interface, alsoProvides, classImplements, implementer, Invalid +from zope.interface import Interface, Invalid, alsoProvides, classImplements, implementer from zope.lifecycleevent import Attributes, ObjectCreatedEvent from zope.location import locate from zope.publisher.interfaces.browser import IBrowserRequest @@ -39,7 +42,8 @@ from pyams_form.group import GroupsBasedForm from pyams_form.interfaces import get_form_weight from pyams_form.interfaces.form import FormCreatedEvent, IAJAXForm, ICustomUpdateSubForm, IForm, \ - IFormContextPermissionChecker, IFormCreatedEvent, IFormLayer, IInnerForm, IInnerSubForm, IInnerTabForm + IFormContextPermissionChecker, IFormCreatedEvent, IFormLayer, IInnerForm, IInnerSubForm, \ + IInnerTabForm from pyams_form.interfaces.form import FormObjectCreatedEvent, FormObjectModifiedEvent from pyams_form.interfaces.form import IAddFormButtons, IEditFormButtons, IModalAddFormButtons, \ IModalDisplayFormButtons, IModalEditFormButtons @@ -52,8 +56,13 @@ from pyams_utils.interfaces import FORBIDDEN_PERMISSION, ICacheKeyValue from pyams_utils.url import absolute_url -from pyams_form import _ + +__docformat__ = 'restructuredtext' +from pyams_form import _ # pylint: disable=ungrouped-imports + + +LOGGER = logging.getLogger('PyAMS (form)') REDIRECT_STATUS_CODES = (300, 301, 302, 303, 304, 305, 307) @@ -96,16 +105,17 @@ @property def title(self): + """Get form's title""" registry = self.request.registry adapter = registry.queryMultiAdapter((self.context, self.request, self), IContentTitle) if adapter is None: adapter = registry.queryAdapter(self.context, IContentTitle) if adapter is not None: return adapter.title - else: - return II18n(self.context).query_attribute('title', request=self.request) + return II18n(self.context).query_attribute('title', request=self.request) def check_mode(self): + """Check form's mode according to context's permissions""" content = self.getContent() # check form permission to get form mode if self.edit_permission and not self.request.has_permission(self.edit_permission, content): @@ -114,52 +124,65 @@ # check form mode based on context checker registry = self.request.registry permission = None - checker = registry.queryMultiAdapter((content, self.request, self), IFormContextPermissionChecker) + checker = registry.queryMultiAdapter((content, self.request, self), + IFormContextPermissionChecker) if checker is None: checker = registry.queryAdapter(content, IFormContextPermissionChecker) if checker is not None: permission = checker.edit_permission if permission and (permission != self.edit_permission): - if (permission == FORBIDDEN_PERMISSION) or not self.request.has_permission(permission, content): + if (permission == FORBIDDEN_PERMISSION) or not self.request.has_permission(permission, + content): self.mode = DISPLAY_MODE def update(self): + """Update form and all it's subforms""" # check form mode self.check_mode() # update form and sub-forms - [subform.update() for subform in self.subforms] - [tabform.update() for tabform in self.tabforms] + [subform.update() for subform in self.subforms] # pylint: disable=expression-not-assigned + [tabform.update() for tabform in self.tabforms] # pylint: disable=expression-not-assigned Form.update(self) # savepoint is required for each inner component to be persisted!! transaction.savepoint() def updateWidgets(self, prefix=None): + """Update form's widgets""" super(BaseForm, self).updateWidgets(prefix) if not self._groups: self.updateGroups() def get_form_action(self): + """Get action associated with form""" return self.action @reify def subforms(self): + """Get subforms adapters associated with this form""" registry = self.request.registry return sorted((adapter[1] - for adapter in registry.getAdapters((self.context, self.request, self), IInnerSubForm)), + for adapter in + registry.getAdapters((self.context, self.request, self), IInnerSubForm)), key=get_form_weight) @reify def tabforms(self): + """Get tabforms adapters associated with this form""" registry = self.request.registry return sorted((adapter[1] - for adapter in registry.getAdapters((self.context, self.request, self), IInnerTabForm)), + for adapter in + registry.getAdapters((self.context, self.request, self), IInnerTabForm)), key=get_form_weight) @property def forms(self): + """Get all forms associated with this form, including the form itself""" return [self, ] + self.subforms + self.tabforms def get_forms(self, include_self=True): + """Get forms associated with this form; if *include_self* argument is True, self is also + included into results list + """ if include_self: yield self for group in self.groups: @@ -173,30 +196,34 @@ @reify def warn_on_change(self): + """JSON boolean value specifying "warn on change" flag""" if self._warn_on_change is True: return 'true' - elif self._warn_on_change is False: + if self._warn_on_change is False: return 'false' - else: - return None + return None @property def is_dialog(self): + """Boolean flag specifying if current form should be displayed in a modal dialog box""" return IDialog.providedBy(self) def get_widget_callback(self, widget): + """Get callback associated with a given widget""" return (self.callbacks or {}).get(widget) # Default z3c.form methods @property def errors(self): + """Get form's errors""" result = [] for form in self.forms: result.extend(form.widgets.errors) return result def add_error(self, error, widget, status=None): + """Add error to current list of form's errors""" if isinstance(error, str): error = Invalid(error) if isinstance(widget, str): @@ -213,13 +240,16 @@ self.status += '\n{0}'.format(translate(error.args[0])) def update_content(self, content, data): + """Update content properties with given data""" changes = applyChanges(self, content, data.get(self, data)) for subform in self.get_forms(include_self=False): if subform.mode == DISPLAY_MODE: continue subform_update = ICustomUpdateSubForm(subform, None) if subform_update is not None: - updates = subform_update.update_content(subform.getContent(), data.get(subform, data)) + # pylint: disable=assignment-from-no-return + updates = subform_update.update_content(subform.getContent(), + data.get(subform, data)) if isinstance(updates, dict): changes.update(updates) else: @@ -227,9 +257,13 @@ return changes def render(self): + """Render form using content template + + Several default templates are defined for all base forms interfaces. + """ request = self.request if isinstance(request, PyramidPublisherRequest): - request = request._request + request = request._request # pylint: disable=protected-access cdict = { 'context': self.context, 'request': request, @@ -242,9 +276,9 @@ if template is None: template = registry.getMultiAdapter((self, request), IContentTemplate) return template(**cdict) - return self.template(**cdict) + return self.template(**cdict) # pylint: disable=not-callable - def __call__(self, **kwargs): + def __call__(self, **kwargs): # pylint: disable=arguments-differ self.update() if self.request.response.status_code in REDIRECT_STATUS_CODES: return Response('') @@ -266,15 +300,23 @@ if layout is None: layout = registry.getMultiAdapter((self, request), ILayoutTemplate) return Response(layout(**cdict)) - return Response(self.layout(**cdict)) + return Response(self.layout(**cdict)) # pylint: disable=not-callable def get_skin(self, request=None): + """Get current skin applied to request""" + if request is None: + request = self.request return request.annotations.get('__skin__') @implementer(IAJAXForm) class AJAXForm(BaseForm): - """AJAX form base class""" + # pylint: disable=abstract-method + """AJAX form base class + + An AJAX form is a form which is submitted through an AJAX call; submit response is + returned as a JSON object which is handled by MyAMS. + """ ajax_handler = FieldProperty(IAJAXForm['ajax_handler']) form_options = FieldProperty(IAJAXForm['form_options']) @@ -282,16 +324,19 @@ ajax_callback = FieldProperty(IAJAXForm['ajax_callback']) def get_form_action(self): + """Get form's target action URL""" return absolute_url(self.context, self.request, self.request.view_name) def get_form_options(self): + """Get form's options""" return json.dumps(self.form_options) if self.form_options else None def get_ajax_handler(self): + """Get form's AJAX handler""" return absolute_url(self.context, self.request, self.ajax_handler) def get_ajax_errors(self, ajax_errors=None): - """Extract form errors in AJAX format""" + """Extract form errors in JSON format""" translate = self.request.localizer.translate errors = { 'status': u'error', @@ -300,8 +345,9 @@ registry = self.request.registry for error in (ajax_errors or self.errors): if isinstance(error, Exception): - error = registry.getMultiAdapter((error, self.request, None, None, self, self.request), - IErrorViewSnippet) + error = registry.getMultiAdapter( + (error, self.request, None, None, self, self.request), + IErrorViewSnippet) error.update() if IMultipleErrors.providedBy(error.error): for inner_error in error.error.errors: @@ -356,6 +402,7 @@ # class AddForm(AJAXForm, BaseAddForm): + # pylint: disable=abstract-method """Add form base class""" prefix = 'add_form.' @@ -373,16 +420,16 @@ def createAndAdd(self, data): registry = self.request.registry # create object - object = self.create(data.get(self, data)) - if IPersistent.providedBy(object): - registry.notify(ObjectCreatedEvent(object)) + obj = self.create(data.get(self, data)) + if IPersistent.providedBy(obj): + registry.notify(ObjectCreatedEvent(obj)) # set parent temporarily to avoid NotYet exceptions - locate(object, self.context) + locate(obj, self.context) # update object properties before adding it - self.update_content(object, data) - self.add(object) - registry.notify(FormObjectCreatedEvent(object, self)) - return object + self.update_content(obj, data) + self.add(obj) + registry.notify(FormObjectCreatedEvent(obj, self)) + return obj def update_content(self, content, data): changes = applyChanges(self, content, data.get(self, data)) @@ -391,6 +438,7 @@ continue subform_update = ICustomUpdateSubForm(subform, None) if subform_update is not None: + # pylint: disable=assignment-from-no-return updates = subform_update.update_content(content, data.get(subform, data)) if isinstance(updates, dict): changes.update(updates) @@ -405,13 +453,15 @@ @property def edit_permission(self): + """Get permission associated with this form's context""" return self.view.edit_permission class AJAXAddForm(AddForm): + # pylint: disable=abstract-method """AJAX add form""" - def __call__(self): + def __call__(self): # pylint: disable=arguments-differ self.request.registry.notify(PageletCreatedEvent(self)) data, errors = {}, () for form in self.get_forms(): @@ -435,8 +485,9 @@ except KeyError: continue else: - view = registry.getMultiAdapter((error, self.request, widget, widget.field, self, self.context), - IErrorViewSnippet) + view = registry.getMultiAdapter( + (error, self.request, widget, widget.field, self, self.context), + IErrorViewSnippet) view.update() widget.error = view errors = (view,) @@ -457,22 +508,23 @@ form_output = form.get_ajax_output(changes) if form_output: for key, value in form_output.items(): - if isinstance(value, (list, tuple)) and (key in output): # concatenate lists + if isinstance(value, (list, tuple)) and ( + key in output): # concatenate lists form_output[key] += output[key] output.update(form_output) except NotImplementedError: pass if output: return output - else: - return { - 'status': 'reload', - 'location': self.nextURL() - } + return { + 'status': 'reload', + 'location': self.nextURL() + } @implementer(IDialog) class DialogAddForm(AddForm): + # pylint: disable=abstract-method """Modal dialog add form""" buttons = Buttons(IModalAddFormButtons) @@ -481,6 +533,7 @@ @implementer(IInnerForm) class InnerAddForm(AddForm): + # pylint: disable=abstract-method """Inner add form""" css_class = 'inner' @@ -528,7 +581,7 @@ class AJAXEditForm(EditForm): """AJAX edit form""" - def __call__(self): + def __call__(self): # pylint: disable=arguments-differ,too-many-branches # call form elements self.request.registry.notify(PageletCreatedEvent(self)) data, errors = {}, () @@ -555,8 +608,9 @@ except KeyError: continue else: - view = registry.getMultiAdapter((error, self.request, widget, widget.field, self, self.context), - IErrorViewSnippet) + view = registry.getMultiAdapter( + (error, self.request, widget, widget.field, self, self.context), + IErrorViewSnippet) view.update() widget.error = view errors = (view,) @@ -593,7 +647,8 @@ form_output = form.get_ajax_output(changes) if form_output: for key, value in form_output.items(): - if isinstance(value, (list, tuple)) and (key in output): # concatenate lists + if isinstance(value, (list, tuple)) and ( + key in output): # concatenate lists form_output[key] += output[key] output.update(form_output) except NotImplementedError: @@ -667,26 +722,28 @@ @subscriber(IFormCreatedEvent, context_selector=ISkinnable) def handle_form_skin(event): + """Apply skin on form's creation event""" request = _request = event.object.request if isinstance(request, PyramidPublisherRequest): - _request = request._request - skin = ISkinnable(event.object).get_skin(_request) + _request = request._request # pylint: disable=protected-access + skin = ISkinnable(event.object).get_skin(_request) # pylint: disable=assignment-from-no-return if skin is not None: apply_skin(request, skin) -class FormSelector(object): +class FormSelector: """Form event selector This selector can be used by subscriber to filter form events """ - def __init__(self, ifaces, config): + def __init__(self, ifaces, config): # pylint: disable=unused-argument if not isinstance(ifaces, (list, tuple)): ifaces = (ifaces,) self.interfaces = ifaces def text(self): + """Form's selector text""" return 'form_selector = %s' % str(self.interfaces) phash = text @@ -702,18 +759,19 @@ return False -class WidgetSelector(object): +class WidgetSelector: """Widget event selector This selector can be used by subscribers to filter widgets events """ - def __init__(self, ifaces, config): + def __init__(self, ifaces, config): # pylint: disable=unused-argument if not isinstance(ifaces, (list, tuple)): ifaces = (ifaces,) self.interfaces = ifaces def text(self): + """Widget's selector text""" return 'widget_selector = %s' % str(self.interfaces) phash = text @@ -729,24 +787,26 @@ return False -class ajax_config(object): +class ajax_config: # pylint: disable=invalid-name """Class decorator used to declare AJAX settings for a form. - When decorating a form class, this decorator create a new subclass which will handle AJAX queries - executed when submitting the form, and register this class as Pyramid's view. + When decorating a form class, this decorator create a new subclass which will handle AJAX + queries executed when submitting the form, and register this class as Pyramid's view. Decorator arguments (all optional) are: - - **name**: AJAX view name - - **context** (or **for_**): view context type - - **layer** (or **request_type**): request type for which view is registered - - **permission**: permission required to call the view; if not set, permission is extracted from form's - "edit_permission" attribute - - **base**: base class for newly created AJAX form; if not set, inherits from :py:class:`AJAXEditForm` - - **implementer**: list of interfaces implemented by the new class - - **method** (or **request_method**): HTTP method name; if not defined, view is restricted to "POST" requests - - **renderer**: name of Pyramid renderer used to return view output; defaults to 'json' - - **xhr**: Pyramid's view's "xhr" predicate; **True** by default + - :param name: AJAX view name + - :param context: (or **for_**): view context type + - :param layer: (or **request_type**): request type for which view is registered + - :param permission: permission required to call the view; if not set, permission is + extracted from form's "edit_permission" attribute + - :param base: base class for newly created AJAX form; if not set, inherits from + :py:class:`AJAXEditForm` + - :param implementer: list of interfaces implemented by the new class + - :param method: (or **request_method**): HTTP method name; if not defined, view is + restricted to "POST" requests + - :param renderer: name of Pyramid renderer used to return view output; defaults to 'json' + - :param xhr: Pyramid's view's "xhr" predicate; **True** by default """ venusian = venusian # for testing injection @@ -773,58 +833,57 @@ settings = self.__dict__.copy() depth = settings.pop('_depth', 0) - def callback(context, name, ob): + def callback(context, name, obj): # pylint: disable=unused-argument cdict = { '__name__': settings.get('name'), - '__module__': ob.__module__, - 'permission': settings.get('permission') or ob.edit_permission + '__module__': obj.__module__, + 'permission': settings.get('permission') or obj.edit_permission } # Set form's AJAX handler - ob.ajax_handler = settings.get('name') + obj.ajax_handler = settings.get('name') # Create new AJAX form and register view base = settings.pop('base') - new_class = type('AJAX' + ob.__name__, (base, ob), cdict) + new_class = type('AJAX' + obj.__name__, (base, obj), cdict) try: # check if current form is overriding "get_ajax_output" method if base not in (AJAXAddForm, AJAXEditForm): # custom base - ob_ajax_parent, _ = ob.get_ajax_output.__qualname__.split('.') + ob_ajax_parent, _ = obj.get_ajax_output.__qualname__.split('.') base_ajax_parent, _ = base.get_ajax_output.__qualname__.split('.') if (ob_ajax_parent != base_ajax_parent) and \ - (ob_ajax_parent in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm')): + (ob_ajax_parent in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm')): new_class.get_ajax_output = base.get_ajax_output else: - new_class.get_ajax_output = ob.get_ajax_output + new_class.get_ajax_output = obj.get_ajax_output else: - base_ajax_parent, _ = ob.get_ajax_output.__qualname__.split('.') - if base_ajax_parent not in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm'): # overriden method - new_class.get_ajax_output = ob.get_ajax_output + base_ajax_parent, _ = obj.get_ajax_output.__qualname__.split('.') + if base_ajax_parent not in ('AJAXForm', 'AJAXAddForm', 'AJAXEditForm'): + # overriden method + new_class.get_ajax_output = obj.get_ajax_output except AttributeError: pass if 'implementer' in settings: - implementer = settings.pop('implementer') - if not isinstance(implementer, (list, tuple, set)): - implementer = (implementer,) - classImplements(new_class, *implementer) + impl = settings.pop('implementer') + if not isinstance(impl, (list, tuple, set)): + impl = (impl,) + classImplements(new_class, *impl) - logger.debug('Registering AJAX view "{0}" for {1} ({2})'.format(settings.get('name'), - str(settings.get('context', Interface)), - str(new_class))) + LOGGER.debug('Registering AJAX view "{0}" for {1} ({2})'.format( + settings.get('name'), str(settings.get('context', Interface)), str(new_class))) - config = context.config.with_package(info.module) + config = context.config.with_package(info.module) # pylint: disable=no-member config.add_view(view=new_class, **settings) - info = self.venusian.attach(wrapped, callback, category='pyams_form', - depth=depth + 1) + info = self.venusian.attach(wrapped, callback, category='pyams_form', depth=depth + 1) - if info.scope == 'class': + if info.scope == 'class': # pylint: disable=no-member # if the decorator was attached to a method in a class, or # otherwise executed at class scope, we need to set an # 'attr' into the settings if one isn't already in there if settings.get('attr') is None: settings['attr'] = wrapped.__name__ - settings['_info'] = info.codeinfo # fbo "action_method" + settings['_info'] = info.codeinfo # pylint: disable=no-member return wrapped diff -r ba372884335f -r c435de184bda src/pyams_form/group.py --- a/src/pyams_form/group.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/group.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,28 +10,32 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - +"""PyAMS_form.group module -# import standard library +This module provides groups management to forms. A group is a set of widgets which are displayed +together inside a fieldset; widgets which are not explicitly associated with a group are affected +to a "default" group. +""" -# import interfaces +from pyramid.decorator import reify +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + from pyams_form.interfaces import get_form_weight from pyams_form.interfaces.form import IFormWidgetsGroup, IGroupsBasedForm, IInnerSubForm from pyams_i18n.interfaces.schema import II18nField -# import packages -from pyramid.decorator import reify -from zope.interface import implementer -from zope.schema.fieldproperty import FieldProperty + +__docformat__ = 'restructuredtext' @implementer(IFormWidgetsGroup) -class FormWidgetsGroup(object): +class FormWidgetsGroup: + # pylint: disable=too-many-instance-attributes """Form widgets group""" form = None - id = FieldProperty(IFormWidgetsGroup['id']) + id = FieldProperty(IFormWidgetsGroup['id']) # pylint: disable=invalid-name widgets = FieldProperty(IFormWidgetsGroup['widgets']) bordered = FieldProperty(IFormWidgetsGroup['bordered']) fieldset_class = FieldProperty(IFormWidgetsGroup['fieldset_class']) @@ -45,10 +49,32 @@ display_mode = FieldProperty(IFormWidgetsGroup['display_mode']) subforms_legend = FieldProperty(IFormWidgetsGroup['subforms_legend']) - def __init__(self, form, id, widgets=None, bordered=True, fieldset_class=None, legend=None, help=None, - css_class='', switch=False, checkbox_switch=False, checkbox_field=None, checkbox_mode='hide', - display_mode='never'): - assert (not checkbox_switch) or checkbox_field, "You must define checkbox field when using checkbox switch" + def __init__(self, form, id, widgets=None, bordered=True, fieldset_class=None, legend=None, + help=None, css_class='', switch=False, checkbox_switch=False, checkbox_field=None, + checkbox_mode='hide', display_mode='never'): + # pylint: disable=too-many-arguments,redefined-builtin,invalid-name + """Form widgets group initialization + + :param form: the form containing the widgets and the group + :param id: the unique ID (inside the form) of the group + :param widgets: the widgets names list associated with the group + :param bordered: if True, the fieldset will have a border + :param fieldset_class: custom fieldset CSS class + :param legend: fieldset legend label + :param help: help text associated with the group + :param css_class: custom group CSS class + :param switch: if True, the fieldset will be switchable + :param checkbox_switch: if True, the switch will be using a checkbox + :param checkbox_field: if *checkbox_switch* is True, you must define the name of the + field which will store the checkbox value + :param checkbox_mode: if set to 'hide', the fieldset content is hidden when the checkbox + is switched; if set to 'disable', the fieldset contents are only disabled + :param display_mode: if set to 'never', fieldset content is never displayed; if set to + 'always', fieldset content is always displayed; if set to 'auto', fieldset content is + displayed when at least one widget value is not default + """ + assert (not checkbox_switch) or checkbox_field, \ + "You must define checkbox field when using checkbox switch" self.form = form self.id = id self.widgets = widgets or [] @@ -65,6 +91,7 @@ @property def css_class(self): + """Group's CSS class""" css_class = self._css_class if self.switch: if self.checkbox_switch: @@ -75,14 +102,18 @@ @reify def checkbox_widget(self): + """Get widget associated with checkbox""" if self.checkbox_field is None: return None for widget in self.widgets: if widget.field is self.checkbox_field: return widget + return None @reify def visible(self): + # pylint: disable=too-many-nested-blocks,too-many-branches + """Get visible status of group""" if self.checkbox_switch: widget = self.checkbox_widget if self.form.ignoreContext: @@ -92,81 +123,113 @@ name = widget.field.getName() value = getattr(widget.field.interface(context), name, None) return bool(value) - else: - if self.display_mode == 'never': - return False - elif (not self.switch) or (self.display_mode == 'always'): - return True - else: # no switch or auto mode - for widget in self.widgets: - if not widget.ignoreContext: - field = widget.field - if self.form.ignoreContext: - value = field.default - else: - context = widget.context - name = field.getName() - value = getattr(field.interface(context), name, None) - if value and (value != field.default): - if II18nField.providedBy(field): - for i18n_value in value.values(): - if i18n_value: - return True - else: + # else: no checkbox switch + if self.display_mode == 'never': + return False + if (not self.switch) or (self.display_mode == 'always'): + return True + # else: no switch or auto mode + for widget in self.widgets: + if not widget.ignoreContext: + field = widget.field + if self.form.ignoreContext: + value = field.default + else: + context = widget.context + name = field.getName() + value = getattr(field.interface(context), name, None) + if value and (value != field.default): + if II18nField.providedBy(field): + for i18n_value in value.values(): + if i18n_value: return True - return False + return True + return False @property def visible_widgets(self): + """Get list of group's visible widgets""" for widget in self.widgets: if (self.checkbox_field is None) or (widget.field is not self.checkbox_field): yield widget @property def switchable(self): + """Can fieldset be switched?""" return self.switch or self.checkbox_switch @property def switcher_state(self): + """Get switcher state""" return 'open' if self.visible else 'off' @property def checker_state(self): + """Get checker state""" return 'on' if self.visible else 'off' @reify def subforms(self): + """Get list of subforms associated with this group""" registry = self.form.request.registry - return sorted((adapter[1] - for adapter in registry.getAdapters((self.form.context, self.form.request, self), - IInnerSubForm)), + return sorted((adapter[1] for adapter in + registry.getAdapters((self.form.context, self.form.request, self), + IInnerSubForm)), key=get_form_weight) def get_forms(self): + """Get list of subforms (including all sub-sub-forms) associated with this group""" for form in self.subforms: for subform in form.get_forms(): yield subform def update(self): + """Update all subforms on group update""" + # pylint: disable=expression-not-assigned [subform.update() for subform in self.subforms] def update_content(self, content, data): + """Update content in all subforms""" + # pylint: disable=expression-not-assigned [subform.update_content(content, data) for subform in self.subforms] -def NamedWidgetsGroup(form, id, widgets, names=(), bordered=True, fieldset_class=None, legend=None, help=None, - css_class='', switch=False, checkbox_switch=False, checkbox_field=None, checkbox_mode='hide', - display_mode='never', factory=FormWidgetsGroup): - """Create a widgets group based on widgets names""" +def NamedWidgetsGroup(form, id, widgets, names=(), bordered=True, fieldset_class=None, + legend=None, help=None, css_class='', switch=False, checkbox_switch=False, + checkbox_field=None, checkbox_mode='hide', display_mode='never', + factory=FormWidgetsGroup): + # pylint: disable=too-many-arguments,redefined-builtin,invalid-name + """Create a widgets group based on widgets names + + :param form: the form containing the widgets and the group + :param id: the unique ID (inside the form) of the group + :param widgets: the widgets names list associated with the group + :param bordered: if True, the fieldset will have a border + :param fieldset_class: custom fieldset CSS class + :param legend: fieldset legend label + :param help: help text associated with the group + :param css_class: custom group CSS class + :param switch: if True, the fieldset will be switchable + :param checkbox_switch: if True, the switch will be using a checkbox + :param checkbox_field: if *checkbox_switch* is True, you must define the name of the + field which will store the checkbox value + :param checkbox_mode: if set to 'hide', the fieldset content is hidden when the checkbox + is switched; if set to 'disable', the fieldset contents are only disabled + :param display_mode: if set to 'never', fieldset content is never displayed; if set to + 'always', fieldset content is always displayed; if set to 'auto', fieldset content is + displayed when at least one widget value is not default + :param factory: widgets group factory + """ if widgets is None: form.updateWidgets() widgets = form.widgets - return factory(form, id, [widgets.get(name) for name in names], bordered, fieldset_class, legend, help, - css_class, switch, checkbox_switch, checkbox_field, checkbox_mode, display_mode) + return factory(form, id, [widgets.get(name) for name in names], bordered, fieldset_class, + legend, help, css_class, switch, checkbox_switch, checkbox_field, + checkbox_mode, display_mode) @implementer(IGroupsBasedForm) -class GroupsBasedForm(object): +class GroupsBasedForm: """Groups based form Should be used as a base class for forms also implementing IForm @@ -179,14 +242,20 @@ self._groups = [] def add_group(self, group): + """Add a group to current form's groups""" self._groups.append(group) @reify def groups(self): + """Get list of groups associated with this form + + If some widgets are associated with any group, a default group is created and located + at the end of groups list. + """ result = self._groups[:] others = [] - if self.widgets: - for widget in self.widgets.values(): + if self.widgets: # pylint: disable=no-member + for widget in self.widgets.values(): # pylint: disable=no-member found = False for group in result: if widget in group.widgets: @@ -200,5 +269,7 @@ css_class=self.main_group_class)) return result - def updateGroups(self): + def updateGroups(self): # pylint: disable=invalid-name + """Update form groups""" + # pylint: disable=expression-not-assigned [group.update() for group in self.groups] diff -r ba372884335f -r c435de184bda src/pyams_form/help.py --- a/src/pyams_form/help.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/help.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,37 +10,43 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - +"""PyAMS_form.help module -# import standard library +This module provides a 'form_help' content provider, which can be used to insert help text +inside any form using an :py:class:`IFormHelp ` adapter. +""" -# import interfaces +from zope.interface import Interface, implementer +from zope.schema.fieldproperty import FieldProperty + from pyams_form.interfaces.form import IFormHelp from pyams_skin.layer import IPyAMSLayer -from pyams_utils.interfaces.text import IHTMLRenderer - -# import packages from pyams_template.template import template_config from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config +from pyams_utils.interfaces.text import IHTMLRenderer from pyams_utils.text import text_to_html -from pyams_viewlet.viewlet import contentprovider_config -from zope.interface import implementer, Interface -from zope.schema.fieldproperty import FieldProperty +from pyams_viewlet.viewlet import ViewContentProvider, contentprovider_config + + +__docformat__ = 'restructuredtext' @contentprovider_config(name='form_help', view=Interface, layer=IPyAMSLayer) @template_config(template='templates/help.pt', layer=IPyAMSLayer) -class HelpContentProvider(object): - """Form help provider""" +class HelpContentProvider(ViewContentProvider): + """Form help content provider""" help = None def update(self): + """Update content provider state""" registry = self.request.registry - help = self.help = registry.queryMultiAdapter((self.context, self.request, self.view), IFormHelp) + # pylint: disable=redefined-builtin + help = self.help = registry.queryMultiAdapter((self.context, self.request, self.view), + IFormHelp) if help is not None: - if help.permission and not self.request.has_permission(help.permission, context=self.context): + if help.permission and not self.request.has_permission(help.permission, + context=self.context): self.help = None elif help.mode and (self.view.mode != help.mode): self.help = None @@ -48,7 +54,7 @@ @implementer(IFormHelp) class FormHelp(ContextRequestViewAdapter): - """Form help""" + """Default form help base implementation""" permission = FieldProperty(IFormHelp['permission']) mode = FieldProperty(IFormHelp['mode']) @@ -62,8 +68,9 @@ @adapter_config(context=(IFormHelp, IPyAMSLayer, Interface), provides=IHTMLRenderer) class HelpRenderer(ContextRequestViewAdapter): - """Help renderer""" + """Default form help renderer""" - def render(self, **kwargs): + def render(self, **kwargs): # pylint: disable=unused-argument + """Render help text to HTML""" message = self.request.localizer.translate(self.context.message) return text_to_html(message, self.context.message_format) diff -r ba372884335f -r c435de184bda src/pyams_form/include.py --- a/src/pyams_form/include.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/include.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,15 +10,15 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' - -# import standard library +"""PyAMS_form.include package -# import interfaces +This package if used for Pyramid integration. +""" -# import packages from pyams_form.form import FormSelector, WidgetSelector +__docformat__ = 'restructuredtext' + def include_package(config): """Pyramid include""" diff -r ba372884335f -r c435de184bda src/pyams_form/interfaces/__init__.py --- a/src/pyams_form/interfaces/__init__.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/interfaces/__init__.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,15 +10,15 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.interfaces module -# import standard library +This namespace module is the base of PyAMS form interfaces. +""" -# import interfaces from .form import IForm, IFormLayer -# import packages +__docformat__ = 'restructuredtext' def get_form_weight(form): """Try to get form weight attribute""" diff -r ba372884335f -r c435de184bda src/pyams_form/interfaces/form.py --- a/src/pyams_form/interfaces/form.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/interfaces/form.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,6 +10,11 @@ # FOR A PARTICULAR PURPOSE. # +"""PyAMS_form.interfaces.form module + +This module provides all custom form-related interfaces. +""" + from pyramid.interfaces import IView from z3c.form import button from z3c.form.interfaces import IButtonWidget, IFormLayer as IBaseFormLayer, INPUT_MODE, \ @@ -28,7 +33,7 @@ __docformat__ = 'restructuredtext' -from pyams_form import _ +from pyams_form import _ # pylint: disable=ungrouped-imports # @@ -147,11 +152,8 @@ def get_widget_callback(self, widget): """Get submit callback associated with a given widget""" - def update_content(self, object, data): - """Update given object with form data""" - - def get_submit_output(self, writer, changes): - """Get submit output""" + def update_content(self, content, data): + """Update given content with form data""" class IAJAXForm(Interface): @@ -192,6 +194,7 @@ form = Object(title="Parent form", schema=IForm) + # pylint: disable=invalid-name id = TextLine(title="Group ID", required=False) @@ -232,16 +235,16 @@ readonly=True) checkbox_mode = Choice(title="Checkbox mode", - description="""To indicate if fieldset content should be hidden or disabled """ - """when the checkbox if not checked""", + description="To indicate if fieldset content should be hidden or " + "disabled when the checkbox if not checked", required=True, values=('disable', 'hide'), default='hide') display_mode = Choice(title="Group display mode", - description="""If 'auto', a switchable group containing only """ - """widgets with default values is hidden; if 'always', group is always - visible; if 'never', group is always hidden""", + description="If 'auto', a switchable group containing only " + "widgets with default values is hidden; if 'always', group " + "is always visible; if 'never', group is always hidden", values=('auto', 'never', 'always'), required=True, default='never') @@ -283,7 +286,7 @@ def add_group(self, group): """Add given group to form groups""" - def updateGroups(self): + def updateGroups(self): # pylint: disable=invalid-name """Update inner groups state""" @@ -358,7 +361,7 @@ class ICustomUpdateSubForm(ISubForm): """SubForm interface with custom update method""" - def update_content(self, object, data): + def update_content(self, object, data): # pylint: disable=redefined-builtin """Custom content update method""" @@ -431,19 +434,19 @@ in some custom contexts to handle form custom settings. """ - def getFields(self): + def getFields(self): # pylint: disable=invalid-name """Get form fields""" - def update(self): + def update(self): # pylint: disable=invalid-name """Update form""" - def updateWidgets(self, prefix=None): + def updateWidgets(self, prefix=None): # pylint: disable=invalid-name """Update form widgets""" - def updateActions(self): + def updateActions(self): # pylint: disable=invalid-name """Update form actions""" - def updateGroups(self): + def updateGroups(self): # pylint: disable=invalid-name """Update form groups""" @@ -459,7 +462,7 @@ if widget.mode == INPUT_MODE: return True if IForm.providedBy(form): - for subform in (form.subforms + form.tabforms): + for subform in form.subforms + form.tabforms: for widget in (subform.widgets or {}).values(): if widget.mode == INPUT_MODE: return True @@ -630,7 +633,7 @@ class FormObjectCreatedEvent(ObjectCreatedEvent): """Form object created event""" - def __init__(self, object, form): + def __init__(self, object, form): # pylint: disable=redefined-builtin super(FormObjectCreatedEvent, self).__init__(object) self.form = form @@ -643,6 +646,6 @@ class FormObjectModifiedEvent(ObjectModifiedEvent): """Form object modified event""" - def __init__(self, object, form, *descriptions): + def __init__(self, object, form, *descriptions): # pylint: disable=redefined-builtin ObjectModifiedEvent.__init__(self, object, *descriptions) self.form = form diff -r ba372884335f -r c435de184bda src/pyams_form/schema.py --- a/src/pyams_form/schema.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/schema.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,19 +10,19 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.schema module + +This module provides an interface and a schema field for form buttons. +""" + +from z3c.form.button import Button +from z3c.form.interfaces import IButton +from zope.interface import implementer +from zope.schema import Bool, TextLine +from zope.schema.fieldproperty import FieldProperty -# import standard library - -# import interfaces -from z3c.form.interfaces import IButton - -# import packages -from z3c.form.button import Button -from zope.interface import implementer -from zope.schema import TextLine, Bool -from zope.schema.fieldproperty import FieldProperty +__docformat__ = 'restructuredtext' class IResetButton(IButton): @@ -44,7 +44,12 @@ class IActionButton(IButton): - """Action button interface""" + """Action button interface + + An action button is a form button which can be used to redirect the user to a "target" URL, + eventually opened in a modal window, or which can call a "click" handler which is the name of + a javascript function. + """ label = TextLine(title="Button label", description="Button label displayed as hint", diff -r ba372884335f -r c435de184bda src/pyams_form/search.py --- a/src/pyams_form/search.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/search.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,30 +10,31 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.search module +This module provides a few helpers which can be used in search forms. +""" -# import standard library +from pyramid.response import Response +from z3c.form import button, field +from z3c.table.interfaces import IValues +from zope.interface import Interface, implementer +from zope.schema import TextLine -# import interfaces -from pyams_form.interfaces.form import IWidgetForm, ISearchForm +from pyams_form.form import AddForm +from pyams_form.interfaces.form import ISearchForm, IWidgetForm +from pyams_form.schema import ResetButton from pyams_skin.interfaces import IContentSearch, IInnerPage, ISearchPage from pyams_skin.layer import IPyAMSLayer -from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION -from z3c.table.interfaces import IValues - -# import packages -from pyams_form.form import AddForm -from pyams_form.schema import ResetButton from pyams_skin.table import BaseTable from pyams_template.template import template_config from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config -from pyramid.response import Response -from z3c.form import field, button -from zope.interface import implementer, Interface -from zope.schema import TextLine +from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION + -from pyams_form import _ +__docformat__ = 'restructuredtext' + +from pyams_form import _ # pylint: disable=ungrouped-imports class ISearchFields(Interface): @@ -52,6 +53,7 @@ @implementer(IWidgetForm, ISearchForm) class SearchForm(AddForm): + # pylint: disable=abstract-method """Base search form""" legend = _("Search") @@ -70,6 +72,9 @@ self.actions['search'].addClass('btn-primary') def get_search_results(self): + """Get search results via an + :py:class:`IContentSearch ` adapter + """ registry = self.request.registry search = registry.queryMultiAdapter((self.context, self.request, self), IContentSearch) if search is None: @@ -78,18 +83,22 @@ search = IContentSearch(self.context, None) if search is not None: self.updateWidgets() - data, errors = self.extractData() + data, errors = self.extractData() # pylint: disable=unused-variable return search.get_search_results(data) + return None @template_config(template='templates/search.pt', layer=IPyAMSLayer) @implementer(IInnerPage, ISearchPage) -class SearchView(object): +class SearchView: """Base search view""" search_form_factory = SearchForm + search_form = None def update(self): + """Update search view""" + # pylint: disable=no-member self.search_form = self.search_form_factory(self.context, self.request) self.search_form.update() @@ -110,5 +119,6 @@ @property def values(self): + """Get view search results""" form = self.view.search_form_factory(self.context, self.request) return form.get_search_results() diff -r ba372884335f -r c435de184bda src/pyams_form/security.py --- a/src/pyams_form/security.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/security.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,16 +10,15 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.security module - -# import standard library +""" -# import interfaces -from pyams_form.interfaces.form import IFormSecurityContext, IFormContextPermissionChecker +from pyramid.decorator import reify -# import packages -from pyramid.decorator import reify +from pyams_form.interfaces.form import IFormContextPermissionChecker, IFormSecurityContext + +__docformat__ = 'restructuredtext' def get_edit_permission(request, context=None): @@ -32,22 +31,42 @@ checker = registry.queryAdapter(context, IFormContextPermissionChecker) if checker is not None: return checker.edit_permission + return None -class ProtectedFormObjectMixin(object): - """Form object protected by a permission""" +class ProtectedFormObjectMixin: + """Form object protected by a permission + + A "protected" form is a form on which you apply a permission; the context on which the security + applies can be provided by an :py:class:`IFormSecurityContext + ` adapter, or will be extracted for the form + context itself. + + The permission itself will be provided by an adapter to :py:class:`IFormContextPermissionChecker + ` + : + + This class is a form mixin class which should be used for forms protected by a + security context. + """ @reify def permission(self): - registry = self.request.registry + """This permission is required to be able to edit the form context""" + request = self.request # pylint: disable=no-member + registry = request.registry checker = None context = IFormSecurityContext(self, None) if context is None: - context = self.context - view = getattr(self, '__parent__', None) or getattr(self, 'view', None) or getattr(self, 'table', None) + context = self.context # pylint: disable=no-member + view = getattr(self, '__parent__', None) or \ + getattr(self, 'view', None) or \ + getattr(self, 'table', None) if view is not None: - checker = registry.queryMultiAdapter((context, self.request, view), IFormContextPermissionChecker) + checker = registry.queryMultiAdapter((context, request, view), + IFormContextPermissionChecker) if checker is None: checker = registry.queryAdapter(context, IFormContextPermissionChecker) if checker is not None: return checker.edit_permission + return None diff -r ba372884335f -r c435de184bda src/pyams_form/terms.py --- a/src/pyams_form/terms.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/terms.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,7 +10,11 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.terms module + +This module is used only to provide a custom ITerms adapter providing translations for +boolean terms. +""" from z3c.form.interfaces import IBoolTerms, IFormLayer, ITerms, IWidget from z3c.form.term import BoolTerms as BaseBoolTerms @@ -19,6 +23,9 @@ from pyams_utils.adapter import adapter_config + +__docformat__ = 'restructuredtext' + from pyams_form import _ diff -r ba372884335f -r c435de184bda src/pyams_form/tests/__init__.py --- a/src/pyams_form/tests/__init__.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/tests/__init__.py Wed Dec 04 11:05:25 2019 +0100 @@ -1,1 +1,31 @@ +# -*- coding: utf-8 -*- ###################################################### +############################################################################## +# +# Copyright (c) 2008-2010 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +""" +Generic Test case for pyams_form doctest +""" +__docformat__ = 'restructuredtext' + +import os +import sys + + +def get_package_dir(value): + """Get package directory""" + + package_dir = os.path.split(value)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + return package_dir diff -r ba372884335f -r c435de184bda src/pyams_form/tests/test_utilsdocs.py --- a/src/pyams_form/tests/test_utilsdocs.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/tests/test_utilsdocs.py Wed Dec 04 11:05:25 2019 +0100 @@ -1,5 +1,7 @@ +# -*- coding: utf-8 -*- ###################################################### +############################################################################## # -# Copyright (c) 2008-2015 Thierry Florac +# Copyright (c) 2008-2010 Thierry Florac # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, @@ -9,22 +11,26 @@ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # +############################################################################## """ -Generic Test case for pyams_form doctest +Generic test case for pyams_form doctests """ + __docformat__ = 'restructuredtext' +import doctest +import os import unittest -import doctest -import sys -import os + +from pyams_utils.tests import get_package_dir -current_dir = os.path.dirname(__file__) +CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) + -def doc_suite(test_dir, setUp=None, tearDown=None, globs=None): - """Returns a test suite, based on doctests found in /doctest.""" +def doc_suite(test_dir, setUp=None, tearDown=None, globs=None): # pylint: disable=invalid-name + """Returns a test suite, based on doctests found in /doctests""" suite = [] if globs is None: globs = globals() @@ -32,15 +38,12 @@ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_ONLY_FIRST_FAILURE) - package_dir = os.path.split(test_dir)[0] - if package_dir not in sys.path: - sys.path.append(package_dir) - + package_dir = get_package_dir(test_dir) doctest_dir = os.path.join(package_dir, 'doctests') # filtering files on extension docs = [os.path.join(doctest_dir, doc) for doc in - os.listdir(doctest_dir) if doc.endswith('.txt')] + os.listdir(doctest_dir) if doc.endswith('.txt') or doc.endswith('.rst')] for test in docs: suite.append(doctest.DocFileSuite(test, optionflags=flags, @@ -50,10 +53,11 @@ return unittest.TestSuite(suite) + def test_suite(): """returns the test suite""" - return doc_suite(current_dir) + return doc_suite(CURRENT_DIR) + if __name__ == '__main__': unittest.main(defaultTest='test_suite') - diff -r ba372884335f -r c435de184bda src/pyams_form/tests/test_utilsdocstrings.py --- a/src/pyams_form/tests/test_utilsdocstrings.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/tests/test_utilsdocstrings.py Wed Dec 04 11:05:25 2019 +0100 @@ -11,17 +11,20 @@ # """ -Generic Test case for pyams_form doc strings +Generic test case for pyams_form docstrings """ + __docformat__ = 'restructuredtext' +import doctest +import os import unittest -import doctest -import sys -import os + +from pyams_utils.tests import get_package_dir -current_dir = os.path.abspath(os.path.dirname(__file__)) +CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) + def doc_suite(test_dir, globs=None): """Returns a test suite, based on doc tests strings found in /*.py""" @@ -32,9 +35,7 @@ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_ONLY_FIRST_FAILURE) - package_dir = os.path.split(test_dir)[0] - if package_dir not in sys.path: - sys.path.append(package_dir) + package_dir = get_package_dir(test_dir) # filtering files on extension docs = [doc for doc in @@ -42,7 +43,7 @@ docs = [doc for doc in docs if not doc.startswith('__')] for test in docs: - fd = open(os.path.join(package_dir, test)) + fd = open(os.path.join(package_dir, test)) # pylint: disable=invalid-name content = fd.read() fd.close() if '>>> ' not in content: @@ -54,9 +55,11 @@ return unittest.TestSuite(suite) + def test_suite(): """returns the test suite""" - return doc_suite(current_dir) + return doc_suite(CURRENT_DIR) + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff -r ba372884335f -r c435de184bda src/pyams_form/viewlet.py --- a/src/pyams_form/viewlet.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/viewlet.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,20 +10,21 @@ # FOR A PARTICULAR PURPOSE. # -__docformat__ = 'restructuredtext' +"""PyAMS_form.viewlet module + +This module provides several viewlet managers which are used into default forms templates. +""" + +from zope.interface import implementer + +from pyams_form.interfaces.form import IFormHeaderViewletsManager, IFormLayer, \ + IFormPrefixViewletsManager, IFormSuffixViewletsManager, IFormToolbarViewletsManager, \ + IFormViewletsManager, IWidgetsPrefixViewletsManager, IWidgetsSuffixViewletsManager +from pyams_template.template import get_view_template, template_config +from pyams_viewlet.manager import WeightOrderedViewletManager, viewletmanager_config -# import standard library - -# import interfaces -from pyams_form.interfaces.form import IFormViewletsManager, IFormPrefixViewletsManager, IWidgetsPrefixViewletsManager, \ - IWidgetsSuffixViewletsManager, IFormSuffixViewletsManager, IFormLayer, IFormHeaderViewletsManager, \ - IFormToolbarViewletsManager - -# import packages -from pyams_template.template import template_config, get_view_template -from pyams_viewlet.manager import WeightOrderedViewletManager, viewletmanager_config -from zope.interface import implementer +__docformat__ = 'restructuredtext' @implementer(IFormViewletsManager) diff -r ba372884335f -r c435de184bda src/pyams_form/widget/__init__.py --- a/src/pyams_form/widget/__init__.py Tue Nov 19 16:30:58 2019 +0100 +++ b/src/pyams_form/widget/__init__.py Wed Dec 04 11:05:25 2019 +0100 @@ -10,6 +10,17 @@ # FOR A PARTICULAR PURPOSE. # +"""PyAMS_form.widget module + +This module provides several custom widgets used inside PyAMS, but also default templates +customized for MyAMS. + +This module also provides several decorators, which can be used to define widgets templates +in place of ZCML declarations. + +**WARNING**: please note that widgets templates used via z3c.form package use default +""" + import inspect import json import locale @@ -54,14 +65,14 @@ __docformat__ = 'restructuredtext' -from pyams_form import _ +from pyams_form import _ # pylint: disable=ungrouped-imports # # Widget template configuration # -class widgettemplate_config(object): +class widgettemplate_config: # pylint: disable=invalid-name """Class decorator used to declare a widget template""" venusian = venusian # for testing injection @@ -75,37 +86,40 @@ def __call__(self, wrapped): settings = self.__dict__.copy() - def callback(context, name, ob): - template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(ob))), + def callback(context, name, obj): # pylint: disable=unused-argument + template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(obj))), settings.get('template')) if not os.path.isfile(template): raise ConfigurationError("No such file", template) - contentType = settings.get('contentType', 'text/html') - factory = WidgetTemplateFactory(template, contentType) + content_type = settings.get('contentType', 'text/html') + factory = WidgetTemplateFactory(template, content_type) provides = settings.get('provides', IPageTemplate) directlyProvides(factory, provides) - config = context.config.with_package(info.module) - config.registry.registerAdapter(factory, - (settings.get('context', Interface), - settings.get('layer', IRequest), - settings.get('view', None), - settings.get('field', None), - settings.get('widget', ob)), - provides, - settings.get('mode', INPUT_MODE)) + registry = settings.get('registry') + if registry is None: + config = context.config.with_package(info.module) # pylint: disable=no-member + registry = config.registry + registry.registerAdapter(factory, + (settings.get('context', Interface), + settings.get('layer', IRequest), + settings.get('view', None), + settings.get('field', None), + settings.get('widget', obj)), + provides, + settings.get('mode', INPUT_MODE)) info = self.venusian.attach(wrapped, callback, category='pyams_form') - if info.scope == 'class': + if info.scope == 'class': # pylint: disable=no-member # if the decorator was attached to a method in a class, or # otherwise executed at class scope, we need to set an # 'attr' into the settings if one isn't already in there if settings.get('attr') is None: settings['attr'] = wrapped.__name__ - settings['_info'] = info.codeinfo # fbo "action_method" + settings['_info'] = info.codeinfo # pylint: disable=no-member return wrapped @@ -120,8 +134,8 @@ settings.get('template')) if not os.path.isfile(template): raise ConfigurationError("No such file", template) - contentType = settings.get('contentType', 'text/html') - factory = WidgetTemplateFactory(template, contentType) + content_type = settings.get('contentType', 'text/html') + factory = WidgetTemplateFactory(template, content_type) provides = settings.get('provides', IPageTemplate) directlyProvides(factory, provides) registry = getGlobalSiteManager() @@ -135,7 +149,7 @@ settings.get('mode', INPUT_MODE)) -class widgetlayout_config(object): +class widgetlayout_config: # pylint: disable=invalid-name """Class decorator used to declare a widget layout""" venusian = venusian # for testing injection @@ -149,37 +163,40 @@ def __call__(self, wrapped): settings = self.__dict__.copy() - def callback(context, name, ob): - template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(ob))), + def callback(context, name, obj): # pylint: disable=unused-argument + template = os.path.join(os.path.dirname(inspect.getfile(inspect.getmodule(obj))), settings.get('template')) if not os.path.isfile(template): raise ConfigurationError("No such file", template) - contentType = settings.get('contentType', 'text/html') - factory = WidgetLayoutFactory(template, contentType) + content_type = settings.get('contentType', 'text/html') + factory = WidgetLayoutFactory(template, content_type) provides = settings.get('provides', IWidgetLayoutTemplate) directlyProvides(factory, provides) - config = context.config.with_package(info.module) - config.registry.registerAdapter(factory, - (settings.get('context', Interface), - settings.get('layer', IRequest), - settings.get('view', None), - settings.get('field', None), - settings.get('widget', ob)), - provides, - settings.get('mode', INPUT_MODE)) + registry = settings.get('registry') + if registry is None: + config = context.config.with_package(info.module) # pylint: disable=no-member + registry = config.registry + registry.registerAdapter(factory, + (settings.get('context', Interface), + settings.get('layer', IRequest), + settings.get('view', None), + settings.get('field', None), + settings.get('widget', obj)), + provides, + settings.get('mode', INPUT_MODE)) info = self.venusian.attach(wrapped, callback, category='pyams_form') - if info.scope == 'class': + if info.scope == 'class': # pylint: disable=no-member # if the decorator was attached to a method in a class, or # otherwise executed at class scope, we need to set an # 'attr' into the settings if one isn't already in there if settings.get('attr') is None: settings['attr'] = wrapped.__name__ - settings['_info'] = info.codeinfo # fbo "action_method" + settings['_info'] = info.codeinfo # pylint: disable=no-member return wrapped @@ -194,8 +211,8 @@ settings.get('template')) if not os.path.isfile(template): raise ConfigurationError("No such file", template) - contentType = settings.get('contentType', 'text/html') - factory = WidgetTemplateFactory(template, contentType) + content_type = settings.get('contentType', 'text/html') + factory = WidgetTemplateFactory(template, content_type) provides = settings.get('provides', IWidgetLayoutTemplate) directlyProvides(factory, provides) registry = getGlobalSiteManager() @@ -224,7 +241,8 @@ @adapter_config(context=(IResetButton, IFormLayer), provides=IFieldWidget) -def ResetFieldWidget(field, request): +def ResetFieldWidget(field, request): # pylint: disable=invalid-name + """Reset buton field widget factory""" reset = FieldWidget(field, ResetWidget(request)) reset.value = field.title return reset @@ -235,8 +253,9 @@ """Reset button action""" def __init__(self, request, field): - Action.__init__(self, request, field.title) - ResetWidget.__init__(self, request) + # pylint: disable=super-init-not-called + Action.__init__(self, request, field.title) # pylint: disable=non-parent-init-called + ResetWidget.__init__(self, request) # pylint: disable=non-parent-init-called self.field = field @@ -255,7 +274,8 @@ @adapter_config(context=(ICloseButton, IFormLayer), provides=IFieldWidget) -def CloseFieldWidget(field, request): +def CloseFieldWidget(field, request): # pylint: disable=invalid-name + """Close button field widget factory""" close = FieldWidget(field, CloseWidget(request)) close.value = field.title return close @@ -266,8 +286,9 @@ """Close button action""" def __init__(self, request, field): - Action.__init__(self, request, field.title) - CloseWidget.__init__(self, request) + # pylint: disable=super-init-not-called + Action.__init__(self, request, field.title) # pylint: disable=non-parent-init-called + CloseWidget.__init__(self, request) # pylint: disable=non-parent-init-called self.field = field @@ -288,7 +309,8 @@ @adapter_config(context=(IActionButton, IFormLayer), provides=IFieldWidget) -def ActionFieldWidget(field, request): +def ActionFieldWidget(field, request): # pylint: disable=invalid-name + """Action button field widget factory""" action = FieldWidget(field, ActionWidget(request)) action.value = field.title return action @@ -299,8 +321,9 @@ """Action button action""" def __init__(self, request, field): + # pylint: disable=super-init-not-called ActionWidget.__init__(self, request) - Action.__init__(self, request, field.title) + Action.__init__(self, request, field.title) # pylint: disable=non-parent-init-called self.field = field self.label_css_class = field.label_css_class self.click_handler = field.click_handler @@ -319,9 +342,11 @@ error_message = _("Invalid UTF-8 encoded data") def toWidgetValue(self, value): + """Convert bytes to string""" return value.decode('utf-8') if isinstance(value, bytes) else value def toFieldValue(self, value): + """Convert string to bytes""" return value.encode('utf-8') if isinstance(value, str) else value @@ -340,11 +365,13 @@ error_message = _("Invalid integer value") def toWidgetValue(self, value): + """Convert integer value to string""" if value is self.field.missing_value: return '' return locale.format_string('%d', value, grouping=True) def toFieldValue(self, value): + """Convert string to integer""" if not value: return self.field.missing_value try: @@ -360,21 +387,25 @@ @property def validate_rules(self): + """Add validation rules to JQuery-validate plug-in""" conv = locale.localeconv() allow_negative = True if (self.field.min is not None) and (self.field.min >= 0): allow_negative = False - rules = {'pattern': '{}[\d{}]+{}'.format( - '\{}?'.format(conv.get('negative_sign', '-')) + rules = { + 'pattern': r'{}[\d{}]+{}'.format( + r'\{}?'.format(conv.get('negative_sign', '-')) if (allow_negative and (conv.get('n_sign_posn') in SIGN_BEFORE_VALUE)) else '', - conv.get('thousands_sep', ''), - '\{}?'.format(conv.get('negative_sign', '-')) - if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')} + conv.get('thousands_sep', ''), + r'\{}?'.format(conv.get('negative_sign', '-')) + if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '') + } return json.dumps(rules) @adapter_config(context=(IInt, IFormLayer), provides=IFieldWidget) -def IntegerFieldWidget(field, request): +def IntegerFieldWidget(field, request): # pylint: disable=invalid-name + """Integer field widget factory""" return FieldWidget(field, IntegerWidget(request)) @@ -389,11 +420,13 @@ error_message = _("Invalid floating value") def toWidgetValue(self, value): + """Convert float to string""" if value is self.field.missing_value: return '' return locale.format_string('%.{0}f'.format(self.widget.precision), value, grouping=True) def toFieldValue(self, value): + """Convert string to float""" if not value: return self.field.missing_value try: @@ -411,23 +444,27 @@ @property def validate_rules(self): + """Add validation rules to JQuery-validate plug-in""" conv = locale.localeconv() allow_negative = True if (self.field.min is not None) and (self.field.min >= 0): allow_negative = False - rules = {'pattern': '{}[\d{}]+{}?\d*{}'.format( - '\{}?'.format(conv.get('negative_sign', '-')) + rules = { + 'pattern': r'{}[\d{}]+{}?\d*{}'.format( + r'\{}?'.format(conv.get('negative_sign', '-')) if (allow_negative and (conv.get('n_sign_posn') in SIGN_BEFORE_VALUE)) else '', - conv.get('thousands_sep', ''), - conv.get('decimal_point', '\.'), - '\{}?'.format(conv.get('negative_sign', '-')) - if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')} + conv.get('thousands_sep', ''), + conv.get('decimal_point', r'\.'), + r'\{}?'.format(conv.get('negative_sign', '-')) + if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '') + } return json.dumps(rules) @adapter_config(context=(IFloat, IFormLayer), provides=IFieldWidget) @adapter_config(context=(IDecimal, IFormLayer), provides=IFieldWidget) -def FloatFieldWidget(field, request): +def FloatFieldWidget(field, request): # pylint: disable=invalid-name + """Float field widget factory""" return FieldWidget(field, FloatWidget(request)) @@ -438,28 +475,33 @@ @property def validate_rules(self): + """Add validation rules to JQuery-validate plug-in""" conv = locale.localeconv() allow_negative = True if (self.field.min is not None) and (self.field.min >= 0): allow_negative = False - rules = {'pattern': '{}[\d{}]+{}?\d*{}'.format( - '\{}?'.format(conv.get('negative_sign', '-')) + rules = { + 'pattern': r'{}[\d{}]+{}?\d*{}'.format( + r'\{}?'.format(conv.get('negative_sign', '-')) if (allow_negative and (conv.get('n_sign_posn') in SIGN_BEFORE_VALUE)) else '', - '', # no thousands separator - '\.', # dot as decimal separator !! - '\{}?'.format(conv.get('negative_sign', '-')) - if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '')} + '', # no thousands separator + r'\.', # dot as decimal separator !! + r'\{}?'.format(conv.get('negative_sign', '-')) + if (allow_negative and (conv.get('n_sign_posn') in SIGN_AFTER_VALUE)) else '') + } return json.dumps(rules) @property def validate_messages(self): + """Get validation message for pattern rule""" return json.dumps({ 'pattern': self.request.localizer.translate(DottedDecimalDataConverter.errorMessage) }) @adapter_config(context=(IDottedDecimalField, IFormLayer), provides=IFieldWidget) -def DottedDecimalFieldWidget(field, request): +def DottedDecimalFieldWidget(field, request): # pylint: disable=invalid-name + """Dotted decimal field widget factory""" return FieldWidget(field, DottedDecimalWidget(request)) @@ -474,7 +516,8 @@ @adapter_config(context=(IDate, IFormLayer), provides=IFieldWidget) -def DateFieldWidget(field, request): +def DateFieldWidget(field, request): # pylint: disable=invalid-name + """Date field widget factory""" return FieldWidget(field, DateWidget(request)) @@ -487,10 +530,12 @@ """Datetime field data converter""" def toWidgetValue(self, value): + """Convert datetime to string""" value = tztime(value) return super(DatetimeDataConverter, self).toWidgetValue(value) def toFieldValue(self, value): + """Convert string to local datetime""" value = super(DatetimeDataConverter, self).toFieldValue(value) return localgmtime(value) @@ -502,7 +547,8 @@ @adapter_config(context=(IDatetime, IFormLayer), provides=IFieldWidget) -def DatetimeFieldWidget(field, request): +def DatetimeFieldWidget(field, request): # pylint: disable=invalid-name + """Datetime field widget factory""" return FieldWidget(field, DatetimeWidget(request)) @@ -517,7 +563,8 @@ @adapter_config(context=(ITime, IFormLayer), provides=IFieldWidget) -def TimeFieldWidget(field, request): +def TimeFieldWidget(field, request): # pylint: disable=invalid-name + """Time field widget factory""" return FieldWidget(field, TimeWidget(request)) @@ -530,12 +577,14 @@ """Color field data converter""" def toWidgetValue(self, value): + """Convert color to string""" value = super(ColorDataConverter, self).toWidgetValue(value) if value: value = '#' + value return value def toFieldValue(self, value): + """Convert string to color value""" if value and value.startswith('#'): value = value[1:] return super(ColorDataConverter, self).toFieldValue(value) @@ -548,7 +597,8 @@ @adapter_config(context=(IColorField, IFormLayer), provides=IFieldWidget) -def ColorFieldWidget(field, request): +def ColorFieldWidget(field, request): # pylint: disable=invalid-name + """Color field widget factory""" return FieldWidget(field, ColorWidget(request)) @@ -564,22 +614,26 @@ @property def editor_data(self): + """Get editor data""" registry = self.request.registry - config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration, name=self.name) + config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration, + name=self.name) if (config is None) and hasattr(self, 'basename'): # I18n widget - config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration, name=self.basename) + config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration, + name=self.basename) # pylint: disable=no-member if config is None: config = registry.queryMultiAdapter((self.form, self.request), ITinyMCEConfiguration) if config is None: config = getattr(self, 'editor_config_data', None) if config is not None: return json.dumps(config) - else: - return json.dumps(config.configuration) + return None + return json.dumps(config.configuration) @adapter_config(context=(IHTMLField, IFormLayer), provides=IFieldWidget) -def HTMLFieldWidget(field, request): +def HTMLFieldWidget(field, request): # pylint: disable=invalid-name + """HTML editor field widget factory""" return FieldWidget(field, HTMLWidget(request)) @@ -592,9 +646,11 @@ """JSON dict data converter""" def toWidgetValue(self, value): + """Convert JSON dict to string""" return value def toFieldValue(self, value): + """Load JSON from string""" return json.loads(value) @@ -604,6 +660,7 @@ """JSON dict field widget""" def get_fields(self): + """Get fields from JSON mapping""" form = self.__parent__.form getter = self.request.registry.queryMultiAdapter((form.getContent(), self.request, form), IJSONDictFieldsGetter) @@ -614,7 +671,7 @@ @adapter_config(context=(IJSONDictField, IFormLayer), provides=IFieldWidget) -def JSONDictFieldWidget(field, request): +def JSONDictFieldWidget(field, request): # pylint: disable=invalid-name """JSON dict field widget factory""" return FieldWidget(field, JSONDictWidget(request)) @@ -623,8 +680,10 @@ # Select2 widget # -@widgettemplate_config(mode=INPUT_MODE, template='templates/select-input.pt', layer=IFormLayer) -@widgettemplate_config(mode=DISPLAY_MODE, template='templates/select-display.pt', layer=IFormLayer) +@widgettemplate_config(mode=INPUT_MODE, template='templates/select-input.pt', + layer=IFormLayer) +@widgettemplate_config(mode=DISPLAY_MODE, template='templates/select-display.pt', + layer=IFormLayer) @implementer_only(ISelect2Widget) class Select2Widget(SelectWidget): """Select2 widget""" @@ -632,17 +691,21 @@ noValueMessage = _("(no selected value)") def get_content(self, entry): + """Get translated entry content""" translate = self.request.localizer.translate return translate(entry['content']) @adapter_config(context=(IChoice, IFormLayer), provides=IFieldWidget) -def ChoiceFieldWidget(field, request): +def ChoiceFieldWidget(field, request): # pylint: disable=invalid-name + """Choice field widget factory""" return FieldWidget(field, Select2Widget(request)) -@widgettemplate_config(mode=INPUT_MODE, template='templates/hidden-select-input.pt', layer=IFormLayer) -@widgettemplate_config(mode=DISPLAY_MODE, template='templates/hidden-select-display.pt', layer=IFormLayer) +@widgettemplate_config(mode=INPUT_MODE, template='templates/hidden-select-input.pt', + layer=IFormLayer) +@widgettemplate_config(mode=DISPLAY_MODE, template='templates/hidden-select-display.pt', + layer=IFormLayer) @implementer(IObjectData) class HiddenSelect2Widget(Select2Widget): """Hidden select2 widget @@ -651,6 +714,7 @@ """ def extract(self, default=NO_VALUE): + """Extract terms from input value""" if self.name not in self.request and self.name + '-empty-marker' in self.request: return [] value = self.request.get(self.name, default) @@ -669,17 +733,21 @@ @property def values(self): + """Convert values list to string""" return '|'.join(self.value or '') @property def values_map(self): + """Create JSON mapping from selected values""" result = {} terms = self.terms + # pylint: disable=expression-not-assigned [result.update({value: terms.getTermByToken(value).title}) for value in self.value or ()] return json.dumps(result) -def HiddenSelect2FieldWidget(field, request): +def HiddenSelect2FieldWidget(field, request): # pylint: disable=invalid-name + """Hidden Select2 field widget factory""" return FieldWidget(field, HiddenSelect2Widget(request)) @@ -690,9 +758,11 @@ """Hidden select2 data converter""" def toWidgetValue(self, value): + """Convert selection to string""" return value or () def toFieldValue(self, value): + """Create set from selected values""" return set(value or ()) @@ -705,25 +775,31 @@ """Text line list field data converter""" def toWidgetValue(self, value): + """Convert values list to string""" return '|'.join(value or []) def toFieldValue(self, value): + """Create list from string""" return value.split('|') if value else None -@widgettemplate_config(mode=INPUT_MODE, template='templates/textlinelist-input.pt', layer=IFormLayer) -@widgettemplate_config(mode=DISPLAY_MODE, template='templates/textlinelist-display.pt', layer=IFormLayer) +@widgettemplate_config(mode=INPUT_MODE, template='templates/textlinelist-input.pt', + layer=IFormLayer) +@widgettemplate_config(mode=DISPLAY_MODE, template='templates/textlinelist-display.pt', + layer=IFormLayer) @implementer_only(ITextLineListWidget) class TextLineListWidget(TextWidget): """Text line list widget""" @property def tags(self): + """Create string from input value""" return json.dumps((self.value or '').split('|')) @adapter_config(context=(ITextLineListField, IFormLayer), provides=IFieldWidget) -def TextLineListFieldWidget(field, request): +def TextLineListFieldWidget(field, request): # pylint: disable=invalid-name + """Textlines list field widget""" return FieldWidget(field, TextLineListWidget(request)) @@ -731,17 +807,22 @@ # TextLine widget with SEO length status # -@widgettemplate_config(mode=INPUT_MODE, template='templates/seo-textline-input.pt', layer=IFormLayer) +@widgettemplate_config(mode=INPUT_MODE, template='templates/seo-textline-input.pt', + layer=IFormLayer) @implementer_only(ISEOTextLineWidget) class SEOTextLineWidget(TextWidget): """SEO textline widget""" @property def length(self): + """Get current length of text""" return len(self.value or '') @property def status(self): + """Get widget status based on text length; a "good" length is between 40 and 66 + characters + """ status = 'success' length = self.length if length < 20 or length > 80: @@ -751,5 +832,6 @@ return status -def SEOTextLineFieldWidget(field, request): +def SEOTextLineFieldWidget(field, request): # pylint: disable=invalid-name + """SEO textline field widget factory""" return FieldWidget(field, SEOTextLineWidget(request))