Skip to content

Commit

Permalink
Merge pull request #31 from RedHatQE/conditional-switchable-view
Browse files Browse the repository at this point in the history
Conditional switchable views.
  • Loading branch information
Milan Falešník committed Jun 7, 2017
2 parents d0a8a8c + e0a41a0 commit 7223406
Show file tree
Hide file tree
Showing 8 changed files with 402 additions and 17 deletions.
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ python:
- '3.5'
- '3.6'
- pypy
install: pip install tox-travis
script: tox
install: pip install -U setuptools tox tox-travis coveralls
script:
- tox
after_success:
- coveralls
deploy:
Expand Down
65 changes: 65 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ Licensed under Apache license, Version 2.0

*WARNING:* Until this library reaches v1.0, the interfaces may change!

Currently the documentation build on RTD is partially broken. You can generate and browse it like
this:

.. code-block:: bash
cd widgetastic.core/ # Your git repository's root folder
tox -e docs
google-chrome gbuild/htmldocs/index.html # Or a browser of your choice
Introduction
------------

Expand All @@ -51,6 +60,7 @@ Features
- Views can define their root locators and those are automatically honoured in the element lookup
in the child Widgets.
- Supports `Parametrized views`_.
- Supports `Switchable conditional views`_.
- Supports `Widget including`_.
- Supports `Version picking`_.
- Supports automatic `Constructor object collapsing`_ for objects passed into the widget constructors.
Expand Down Expand Up @@ -320,3 +330,58 @@ including. So when instantiated, the underlying ``FormButtonsAdd`` has the same
the ``ItemAddForm``. I did not think it would be wise to make the including widget a parent for the
included widgets due to the fact widgetastic fences the element lookup if ``ROOT`` is present on a
widget/view.


.. `Switchable conditional views`:
Switchable conditional views
----------------------------

If you have forms in your product whose parts change depending on previous selections, you might
like to use the ``ConditionalSwitchableView``. It will allow you to represent different kinds of
views under one widget name. An example might be a view of items that can use icons, table, or
something else. You can make views that have the same interface for all the variants and then
put them together using this tool. That will allow you to interact with the different views the
same way. They display the same informations in the end.

.. code-block:: python
class SomeForm(View):
foo = Input('...')
action_type = Select(name='action_type')
action_form = ConditionalSwitchableView(reference='action_type')
# Simple value matching. If Action type 1 is selected in the select, use this view.
# And if the action_type value does not get matched, use this view as default
@action_form.register('Action type 1', default=True)
class ActionType1Form(View):
widget = Widget()
# You can use a callable to declare the widget values to compare
@action_form.register(lambda action_type: action_type == 'Action type 2')
class ActionType2Form(View):
widget = Widget()
# With callable, you can use values from multiple widgets
@action_form.register(
lambda action_type, foo: action_type == 'Action type 2' and foo == 2)
class ActionType2Form(View):
widget = Widget()
You can see it gives you the flexibility of decision based on the values in the view.

This example as shown (with Views) will behave like the ``action_form`` was a nested view. You can
also make a switchable widget. You can use it like this:

.. code-block:: python
class SomeForm(View):
foo = Input('...')
bar = Select(name='bar')
switched_widget = ConditionalSwitchableView(reference='bar')
switched_widget.register('Action type 1', default=True, widget=Widget())
Then instead of switching views, it switches widgets.
9 changes: 9 additions & 0 deletions src/widgetastic/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ def __new__(cls, *args, **kwargs):

@property
def child_items(self):
"""If you implement your own class based on :py:class:`Widgetable`, you need to override
this property.
This property tells the widget processing system all instances of
:py:class:`WidgetDescriptor` that this object may provide. That system then in turn makes
sure that the appropriate entries in name/descriptor mapping are in place so when the
descriptor gets instantiated, it can find its name in the mapping, making the instantiation
possible.
"""
return []


Expand Down
148 changes: 148 additions & 0 deletions src/widgetastic/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -1830,3 +1830,151 @@ def fill(self, item_or_items):
self.select_by_value(*values_to_select)

return bool(options_to_select or values_to_select or deselected)


class ConditionalSwitchableView(Widgetable):
"""Conditional switchable view implementation.
This widget proxy is useful when you have a form whose parts displayed depend on certain
conditions. Eg. when you select certain value from a dropdown, one form is displayed next,
when other value is selected, a different form is displayed next. This widget proxy is designed
to register those multiple views and then upon accessing decide which view to use based on the
registration conditions.
The resulting widget proxy acts similarly like a nested view (if you use view of course).
Example:
.. code-block:: python
class SomeForm(View):
foo = Input('...')
action_type = Select(name='action_type')
action_form = ConditionalSwitchableView(reference='action_type')
# Simple value matching. If Action type 1 is selected in the select, use this view.
# And if the action_type value does not get matched, use this view as default
@action_form.register('Action type 1', default=True)
class ActionType1Form(View):
widget = Widget()
# You can use a callable to declare the widget values to compare
@action_form.register(lambda action_type: action_type == 'Action type 2')
class ActionType2Form(View):
widget = Widget()
# With callable, you can use values from multiple widgets
@action_form.register(
lambda action_type, foo: action_type == 'Action type 2' and foo == 2)
class ActionType2Form(View):
widget = Widget()
You can see it gives you the flexibility of decision based on the values in the view.
Args:
reference: For using non-callable conditions, this must be specified. Specifies the name of
the widget whose value will be used for comparing non-callable conditions.
"""
def __init__(self, reference=None):
self.reference = reference
self.registered_views = []
self.default_view = None

@property
def child_items(self):
return [
descriptor
for _, descriptor
in self.registered_views
if isinstance(descriptor, WidgetDescriptor)]

def register(self, condition, default=False, widget=None):
"""Register a view class against given condition.
Args:
condition: Condition check for switching to appropriate view. Can be callable or
non-callable. If callable, then callable parameters are resolved as values from
widgets resolved by the argument name, then the callable is invoked with the params.
If the invocation result is truthy, that view class is used. If it is a non-callable
then it is compared with the value read from the widget specified as ``reference``.
default: If no other condition matches any registered view, use this one. Can only be
specified for one registration.
widget: In case you do not want to use this as a decorator, you can pass the widget
class or instantiated widget as this parameter.
"""
def view_process(cls_or_descriptor):
if not (
isinstance(cls_or_descriptor, WidgetDescriptor) or
(inspect.isclass(cls_or_descriptor) and issubclass(cls_or_descriptor, Widget))):
raise TypeError(
'Unsupported object registered into the selector (!r})'.format(
cls_or_descriptor))
self.registered_views.append((condition, cls_or_descriptor))
if default:
if self.default_view is not None:
raise TypeError('Multiple default views specified')
self.default_view = cls_or_descriptor
# We explicitly return None
return None
if widget is None:
return view_process
else:
return view_process(widget)

def __get__(self, o, t):
if o is None:
return self

condition_arg_cache = {}
for condition, cls_or_descriptor in self.registered_views:
if not callable(condition):
# Compare it to a known value (if present)
if self.reference is None:
# No reference to check against
raise TypeError(
'reference= not set so you cannot use non-callables as conditions')
else:
if self.reference not in condition_arg_cache:
try:
condition_arg_cache[self.reference] = getattr(o, self.reference).read()
except AttributeError:
raise TypeError(
'Wrong widget name specified as reference=: {}'.format(
self.reference))
if condition == condition_arg_cache[self.reference]:
view_object = cls_or_descriptor
break
else:
# Parse the callable's args and inject the correct args
c_args, c_varargs, c_keywords, c_defaults = inspect.getargspec(condition)
if c_varargs or c_keywords or c_defaults:
raise TypeError('You can only use simple arguments in lambda conditions')
arg_values = []
for arg in c_args:
if arg not in condition_arg_cache:
try:
condition_arg_cache[arg] = getattr(o, arg).read()
except AttributeError:
raise TypeError(
'Wrong widget name specified as parameter {}'.format(arg))
arg_values.append(condition_arg_cache[arg])

if condition(*arg_values):
view_object = cls_or_descriptor
break
else:
if self.default_view is not None:
view_object = self.default_view
else:
raise ValueError('Could not find a corresponding registered view.')
if inspect.isclass(view_object):
view_class = view_object
else:
view_class = type(view_object)
o.logger.info('Picked %s', view_class.__name__)
if isinstance(view_object, Widgetable):
# We init the widget descriptor here
return view_object.__get__(o, t)
else:
return view_object(o, additional_context=o.context)
8 changes: 0 additions & 8 deletions testing/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import allure
import pytest

import codecs
Expand Down Expand Up @@ -38,10 +37,3 @@ def browser(selenium, httpserver, request):
httpserver.serve_content(codecs.open(testfilename, mode='r', encoding='utf-8').read())
selenium.get(httpserver.url)
return CustomBrowser(selenium)


def pytest_exception_interact(node, call, report):
if selenium_browser is not None:
allure.attach(
'Error screenshot', selenium_browser.get_screenshot_as_png(), allure.attach_type.PNG)
allure.attach('Error traceback', str(report.longrepr), allure.attach_type.TEXT)
Loading

0 comments on commit 7223406

Please sign in to comment.