docs/main/MokshaContainerWidgets.rst | 203 ++++++++ moksha/api/widgets/containers/tabbedcontainer.py | 28 - moksha/api/widgets/containers/templates/dashboardcontainer.mak | 2 moksha/lib/helpers.py | 238 ++++++++-- 4 files changed, 428 insertions(+), 43 deletions(-)
New commits: commit 8ca724eb7825f3f40009363dbff933e926db4926 Merge: 148356d... a9febba... Author: John (J5) Palmieri johnp@redhat.com Date: Tue Feb 3 15:51:38 2009 -0500
Merge branch 'master' of git+ssh://johnp@git.fedorahosted.org/git/moksha
commit 148356d35522065b269d6c6abf359f53c0029096 Author: John (J5) Palmieri johnp@redhat.com Date: Tue Feb 3 15:50:55 2009 -0500
add documentation to container widgets
diff --git a/docs/main/MokshaContainerWidgets.rst b/docs/main/MokshaContainerWidgets.rst new file mode 100644 index 0000000..839d07c --- /dev/null +++ b/docs/main/MokshaContainerWidgets.rst @@ -0,0 +1,203 @@ +Moksha Container Widgets +======================== + +Moksha container widgets provide configurable containers for loading Moksha +applications and widgets. They are used for layout, navigation and dynamic +loading of content on your site. The configuration format for specifying +components to load into containers make it easy to dynamically store and update +configuration information including location and default settings of an +application. Containers can load applications asynchronously through JavaScript +or pull in a widget during the initial request. Since containers are widgets +themselves containers can be nested. + +Configuration Format +-------------------- + +Configuration for each container is done using a restricted python syntax and +can be passed to the container as a string or a Python object. Strings are +mainly used when reading from a configuration file but can also be used to avoid +having to import the configuration wrapper objects. + +Here is an example of what a configuration line could look like: + +.. code-block:: python + + dashboard_layout = + [Category('Test Applications', [ + MokshaApp('Hello World 1', + 'moksha.helloworld', + {'name':'J5'}), + App('Hello World 2', + '/appz/moksha.helloworld', + {'name':'Luke'}), + MokshaWidget('Hello World 3', + 'moksha.helloworldwidget', + {'name':'Anonymous'}, auth=Not(not_anonymous())) + ] + ) + ] + +This would be a configuration for the dashboard container discussed bellow. +It defines one category, two applications and a widget. How this is laid out +in your final view is up to the template and css. The default view is setup +to load each component into it's category div in order. You can then specify +CSS to position those categories in your layout. + +Lets look at each individual component. + +* Category 'Test Applications' - This tells the container that you want to group +a number of applications under the 'Test Application' title which in essence +creates a div with class 'test_applications'. You may also want to specify +css_class instead of relying on the label. + +* MokshaApp 'Hello World 1': here we dynamically load the moksha application +installed on the ``moksha.application`` entry point as ``moksha.helloworld``. +We then send it a dictionary of keys - in this case the key 'name'. + +* App 'Hello World 2': This is the same as the above but instead of giving +the application name we give it the url the application is mounted on. We +also send in different configuration data. The App object allows for non +moksha web apps and static content to be placed in a container. + +* MokshaWidget 'Hello World 3': Again this is the same as above but we pass in +a widget installed on the ``moksha.widget`` entry point as +``moksha.helloworldwidget``. We also pass in a auth predicate. If the +authorization evaluates to True, in this case if the user is anonymous, the +widget will be embedded in the page. If not then it is removed. Predicates +are defined by the :module:`repoze.what.authorize` module and can be added to +any ConfigWrapper. + +Tabbed Container +---------------- + +:class:`moksha.api.widgets.containers.TabbedContainer` + +A tabbed container is a container that places components in a jQuery.ui.tabs +javascript widget. Applications are then dynamically loaded when that tab is +selected. Widgets placed in a tabbed container are continuously running while +the tabbed container is active even if not visible. It is important to not load +widgets that have a high resource requirement. Tabbed containers must be +subclassed in order to point it to the correct resources. + +Here is an example on how to subclass a TabbedContainer: + +mainnav.py +.. code-block:: python + from moksha.api.widgets.containers import TabbedContainer + + class MainNav(TabbedContainer): + template = 'mako:myapp.templates.mainnav' + config_key = 'myapp.mainnav.apps' + +mainnav.mak +.. code-block:: html + <div> + <ul id="${id}"> + % for t in tabs: + <li> + % if t.has_key('url'): + <a href="${t['url']}" title="${t['label']} Page"> + ${t['label']} + </a> + % else + ${t['label']} + % endif + </li> + % endfor + </ul> + </div> + <div id="content"> + % for t in tabs: + <div id="${t['label']}_Page"> + % if t.has_key('widget'): + ${t['widget'](t['params'])} + % endif + </div> + % endfor + </div> + +development.ini +.. code-block:: python + [DEFAULT] + myapp.mainnav.apps = (MokshaApp('Home', 'myapp.home'), + MokshaApp('2nd Tab', 'myapp.tab2'), + MokshaApp('3rd Tab','myapp.tab3', + auth=not_anonymous()), + MokshaApp('4th Tab', 'myapp.tab4', + auth=Not(not_anonymous()) + ) + ) + +It should be noted that the template boilerplate should be handled automatically +in the future. + +Dashboard Container +------------------- + +:class:`moksha.api.widgets.containers.DashboardContainer` + +A dashboard container is a container that places components in a +jQuery.ui.sortable javascript widget. Applications are dynamically loaded in +the order they are placed in the configuration. Dashboard containers must be +subclassed in order to point it to the correct resources. + +Here is an example on how to subclass a DashboardContainer: + +mainnav.py +.. code-block:: python + from moksha.api.widgets.containers import DashboardContainer + + class HomePageContainer(DashboardContainer): + template = 'mako:myapp.templates.homepagecontainer' + layout = [Category('left-content-column', + [App('Banner', '/static-html/sitebanner.html'), + MokshaApp('Stable Updates','myapp.updates/table', + {"some_json":'{"status":"stable"}'} + ), + MokshaApp('Testing Updates','myapp.updates/table', + {"some_json":'{"status":"testing"}'} + ), + ]), + Category('right-content-column', + MokshaWidget(None, 'myapp.loginwidget', + auth=Not(not_anonymous()) + ) + ) + ] + +homepagecontainer.mak +.. code-block:: html + <div id="${id}"> + <div> + <div id="right-content-column"> + ${applist_widget(category = 'right-content-column', layout = layout)} + </div> + <div id="left-content-column"> + ${applist_widget(category = 'left-content-column', layout = layout)} + </div> + </div> + </div> + +Notice above that I decided to use the layout calls variable instead of a +configuration key. Either form is acceptable for any container. + +Issues +------ + +Moving from a model where you piece everything together on the server to +dynamically loading content in the browser means that there are some issues +to consider. + +* Id's may clash. It is suggested that when using jQuery +or any other javascript dom tool to generate a uuid and do all of your +selections relative to that id. It is also suggested you namespace your id's +and only use classes to style. + +* Javascript may load more than once. If all you uses is widgets you are fine +as ToscaWidgets will take care of duplicate resource requests. However a +powerful concept in moksha is the ability to load applications asynchronously +so that the user does not have to wait for the server to finish processing a +page before any data is streamed to them. It is suggested you make heavy use +of global resources in order to aleviate the issue. At some point we may +introduce a way for the browser to filter out already loaded javascript and +other resources. \ No newline at end of file diff --git a/moksha/api/widgets/containers/tabbedcontainer.py b/moksha/api/widgets/containers/tabbedcontainer.py index 247cd98..1111084 100644 --- a/moksha/api/widgets/containers/tabbedcontainer.py +++ b/moksha/api/widgets/containers/tabbedcontainer.py @@ -8,22 +8,26 @@ import urllib """ :Name: TabbedContainer :Type: Container - -:Notes: We may need to create a tab javascript object that inherits from - jQuery.ui.tabs """ class TabbedContainer(JQueryUITabs): """ :tabs: An ordered list of application tabs to display - Application descriptors can come is a couple of forms - - * tuple - (label, url, {request parameters}) - * App class - App(label = label, - url = url, - req_params = {request parameters}) - * MokshaApp class - MokshaApp(label = label, - application = moksha app name, - req_params = {request parameters}) + Application descriptors come from the config wrappers in + moksha.lib.helpers + + tabs can either be in serialized string format or as a list of + config wrapper objects. Using strings means you don't have to + import the wrappers and predicates but can get unwieldy if there + is a long list of wrappers + + :config_key: the configuration key used to store the serialized tab config + in a configuration file instead of embeding it in the widget + + :template: you must provide a template in order to get styling correct. The + default template has minimal functionality. The documentation + for jQuery.UI.Tabs can be found at http://ui.jquery.org. + FIXME: Write a tutorial and provide helper widgets so + creating a template becomes really easy. """ css=[] # remove the default css template = 'mako:moksha.api.widgets.containers.templates.tabbedcontainer' diff --git a/moksha/api/widgets/containers/templates/dashboardcontainer.mak b/moksha/api/widgets/containers/templates/dashboardcontainer.mak index 265bfa1..dd4e57d 100644 --- a/moksha/api/widgets/containers/templates/dashboardcontainer.mak +++ b/moksha/api/widgets/containers/templates/dashboardcontainer.mak @@ -1,7 +1,7 @@ <div> Sample Template - you should subclass and create your own layout <div id="container"> - % for c in layout: + % for c in layout: ${applist_widget(category = c)} % endfor </div> diff --git a/moksha/lib/helpers.py b/moksha/lib/helpers.py index 26510ec..1171aec 100644 --- a/moksha/lib/helpers.py +++ b/moksha/lib/helpers.py @@ -1,8 +1,8 @@ from webhelpers import date, feedgenerator, html, number, misc, text
-from repoze.what.predicates import (Not, Predicate, All, Any, has_all_permissions, - has_any_permission, has_permission, - in_all_groups, in_any_group, in_group, +from repoze.what.predicates import (Not, Predicate, All, Any, has_all_permissions, + has_any_permission, has_permission, + in_all_groups, in_any_group, in_group, is_user, not_anonymous)
import urllib @@ -15,8 +15,32 @@ import moksha scrub_filter = re.compile('[^_a-zA-Z0-9-]')
class ConfigWrapper(object): + """ Base class for container configuration wrappers + + Configuration Wrappers are python objects which can be embedded in + configuration stores (files and databases) or used directly to + insert web applications into containers. Wrappers also integrate + repoze.what predicates which are checked when the wrapper is processed. + If the prediates return False the web application is not displayed to + the user. This makes it easy to build composite Moksha applications + where what the user is show is dictated by their authorization level. + + Configuration wrapper are only standard data structures with the ability + to process predicates. A container widget is needed to convert the + configuration into rendered applications on a page. + + Derive from this class to create new configuration syntax + + """ + @staticmethod def process_wrappers(wrappers): + """ Helper method for evlauating a list of wrappers + + :returns: a list of hashes for each of the application + that will be displayed + """ + result = [] if isinstance(wrappers, ConfigWrapper): w = wrappers.process() @@ -27,39 +51,100 @@ class ConfigWrapper(object): w = item.process() if w: result.append(w) - + return result - + def process(self): + """Override this in derived classes to return a hash representing + the configuration option + """ return None
class Category(ConfigWrapper): - def __init__(self, label="", apps=None, auth=None): + """A configuration wrapper class that displays a list of application + and/or widget wrappers + + :Example: + left_column = Category("Left Column Apps", + [App(...), App(...)], + not_anonymous()) + """ + + def __init__(self, label="", apps=None, css_class=None, auth=None): + """ + :label: The category heading the container should use when rendering + a category. Labels for categories are not rendered in the + default template. If a css_class is not defined the label + becomes the css class for the div once it has been scrubbed + by making it all lower case and replacing illegal characters + with underscores. + :apps: A list of wrappers representing application configurations + :auth: A list of predicates which are evaluate before the wrapper + is sent to the container. If it evaluates to False it does not + get sent. + :css_class: Either a string or list of strings defining the css class + for this category + """ + self.label = label self.apps = apps or [] self.auth = auth or [] - - def process(self): + if not css_class: + css_class = scrub_filter.sub('_', self.label.lower()) + elif isinstance(css_class, list) or isinstance(css_class, tuple): + css_class = ' '.join(css_class) + + self.css_class = css_class + + def process(self): + """Check the predicates and construct the dict + + :returns: a dict with all the configuration data for this category + """ + if not check_predicates(self.auth): return None - + id = uuid.uuid4() - css_class = scrub_filter.sub('_', self.label.lower())
apps = self.process_wrappers(self.apps) return {'label': self.label, 'apps': apps, 'id': id, 'css_class': css_class}
class App(ConfigWrapper): + """A configuration wrapper class that displays an application pointed to + by a url + + :Example: + hello_app = App('Hello World App', + '/appz/moksha.helloworld', + {'greeter_name': 'J5'}, + not_anonymous()) + """ def __init__(self, label="", url="", req_params=None, auth=None): + """ + :label: The title the application should use when rendering + in a container. Leave this blank if you do not wish the + application to be visibly labeled when rendered + :url: the url to load this application from + :req_params: A dict of request parameters to send to the application + when loading it from a url + :auth: A list of predicates which are evaluate before the wrapper + is sent to the container. If it evaluates to False it does not + get sent. + """ self.label = label self.url = url self.req_params = req_params or {} self.auth = auth or [] - + def process(self): + """Check the predicates and construct the dict + + :returns: a dict with all the configuration data for this application + """ if not check_predicates(self.auth): return None - + query_str = "" if self.req_params: query_str = "?" + urllib.urlencode(self.req_params) @@ -69,50 +154,131 @@ class App(ConfigWrapper): return {'label': self.label, 'url': self.url + query_str, 'id': id}
class MokshaApp(App): + """A configuration wrapper class that displays a Moksa application + selected by name. This differes from App in that you don't have to + know the url, only the name the application is mounted under. In + future versions we may also try and get the auth predicated from + the application's own configuration. + + :Example: + hello_app = App('Hello World Moksha App', + 'moksha.helloworld', + {'greeter_name': 'J5'}, + not_anonymous()) + + """ + def __init__(self, label="", moksha_app="", req_params=None, auth=None): + """ + :label: The title the application should use when rendering + in a container. Leave this blank if you do not wish the + application to be visibly labeled when rendered + :moksha_app: the name of the entry point registered under the + moksha.application section + :req_params: A dict of request parameters to send to the application + when loading it + :auth: A list of predicates which are evaluate before the wrapper + is sent to the container. If it evaluates to False it does not + get sent. + """ # FIXME figure out how to pull auth info from an app super(MokshaApp, self).__init__(label, '/appz/' + moksha_app, req_params, auth)
class Widget(ConfigWrapper): + """A configuration wrapper class that displays a ToscaWidget. Use this + instead of an App when the time it takes to load an application from a url + is more than the time it takes to just embed it in the template. Widgets + also lack the ability to respond to remote requests via controllers. + + :Example: + class HelloWidget(tw.api.Widget): + pass + + hello_widget = Widget('Hello World Widget', + HelloWidget(), + {'greeter_name': 'J5'}, + not_anonymous()) + + """ def __init__(self, label="", widget="", params=None, auth=None): + """ + :label: The title the widget should use when rendering + in a container. Leave this blank if you do not wish the + widget to be visibly labeled when rendered + :widget: the ToscaWidget to be rendered + :params: A dict of parameters to send to the widgets update_params + method when rendering it + :auth: A list of predicates which are evaluate before the wrapper + is sent to the container. If it evaluates to False it does not + get sent. + """ self.label = label self.widget = widget self.params = params or {} self.auth = auth or [] - + def process(self): if not check_predicates(self.auth): return None - + id = uuid.uuid4()
- return {'label': self.label, 'widget': self.widget , 'params':self.params, 'id': id} + return {'label': self.label, 'widget': self.widget , 'params':self.params, 'id': id}
class MokshaWidget(Widget): + """A configuration wrapper class that displays a ToscaWidget registered + in Moksha's widget dictionary. + + :Example: + hello_moksha_widget = Widget('Hello Moksha Widget', + 'moksha.hello', + {'greeter_name': 'J5'}, + not_anonymous()) + + """ def __init__(self, label="", name="", params=None, auth=None): - print "********************** ", moksha._widgets + """ + :label: The title the widget should use when rendering + in a container. Leave this blank if you do not wish the + widget to be visibly labeled when rendered + :name: the name of the entry point registered under the + moksha.widget section + :params: A dict of parameters to send to the widgets update_params + method when rendering it + :auth: A list of predicates which are evaluate before the wrapper + is sent to the container. If it evaluates to False it does not + get sent. + """ widget = moksha._widgets[name]['widget'] - super(MokshaWidget, self).__init__(label=label, widget=widget, params=params, auth=auth) + super(MokshaWidget, self).__init__(label=label, widget=widget, params=params, auth=auth)
+ +# setup the dictionary of acceptable callables when eval'ing predicates from +# a configuration file - this makes up the keywords of the predicate +# configuration format _safe_predicate_callables = { 'Not': Not, 'All': All, - 'Any': Any, - 'has_all_permissions': has_all_permissions, - 'has_any_permission': has_any_permission, - 'has_permission': has_permission, - 'in_all_groups': in_all_groups, - 'in_any_group': in_any_group, - 'in_group': in_group, - 'is_user': is_user, + 'Any': Any, + 'has_all_permissions': has_all_permissions, + 'has_any_permission': has_any_permission, + 'has_permission': has_permission, + 'in_all_groups': in_all_groups, + 'in_any_group': in_any_group, + 'in_group': in_group, + 'is_user': is_user, 'not_anonymous': not_anonymous}
+# setup the dictionary of acceptable callables when eval'ing config wrappers +# from a configuration file - this makes up the keywords of the config wrapper +# configuration format _app_config_callables = {'Category': Category, 'MokshaApp': MokshaApp, 'Widget': Widget, 'App': App, 'MokshaWidget': MokshaWidget}
+# config wrapper configuration can also utilize predicates _app_config_callables.update(_safe_predicate_callables)
@@ -120,24 +286,28 @@ def eval_app_config(config_str): """ Safely evaluates application configuration strings for storing application layout data in a moksha configuration file + + :return: the evaluated configuration wrapper configuration """ return eval(config_str, {"__builtins__":None}, _app_config_callables)
def eval_predicates(predicate_str): - """ + """ Safely evaluates a string of predicates + + :return: the evaluated predicate configuration """ return eval(predicate_str, {"__builtins__":None}, _safe_predicate_callables)
def check_predicates(predicates): """ - Using the current WSGI environment evaluate a list of predicates. + Using the current WSGI environment run a list of predicates. This can only be used when inside a WSGI request
- Return False is any one is False - Return True if they are all True + :return: False is any one is False + :return: True if they are all True """
from pylons import request @@ -148,7 +318,7 @@ def check_predicates(predicates): return False return True
- + if(isinstance(predicates, Predicate)): return predicates.eval_with_environ(request.environ)
@@ -156,6 +326,14 @@ def check_predicates(predicates):
def eval_and_check_predicates(predicate_str): + """ + Using the current WSGI environment evaluate a predicate sting and run + the resulting predicate list. This can only be used when inside a WSGI + request + + :return: False is any one is False + :return: True if they are all True + """ p = eval_predicates(predicate_str) return check_predicates(p)