Commits

Anonymous committed 253f5f5

[svn] Initial version of tw.dynforms

Comments (0)

Files changed (42)

+[egg_info]
+tag_build = dev
+tag_svn_revision = true
+import os
+import sys
+from fnmatch import fnmatchcase
+from distutils.util import convert_path
+
+from setuptools import setup, find_packages
+
+
+def find_package_data( package='', where='.', only_in_packages=True):
+    """Finds static resources in package. Adapted from turbogears.finddata."""  
+    out = {}
+    exclude = ('*.py', '*.pyc', '*~', '.*', '*.bak', '*.swp*')
+    exclude_directories = ('.*', 'CVS', '_darcs', './build',
+                           './dist', 'EGG-INFO', '*.egg-info')
+    stack = [(convert_path(where), '', package, only_in_packages)]
+    while stack:
+        where, prefix, package, only_in_packages = stack.pop(0)
+        for name in os.listdir(where):
+            fn = os.path.join(where, name)
+            if os.path.isdir(fn):
+                bad_name = False
+                for pattern in exclude_directories:
+                    if (fnmatchcase(name, pattern)
+                        or fn.lower() == pattern.lower()):
+                        bad_name = True
+                        print >> sys.stderr, (
+                            "Directory %s ignored by pattern %s"
+                            % (fn, pattern))
+                        break
+                if bad_name:
+                    continue
+                if os.path.isfile(os.path.join(fn, '__init__.py')):
+                    if not package:
+                        new_package = name
+                    else:
+                        new_package = package + '.' + name
+                    stack.append((fn, '', new_package, False))
+                else:
+                    stack.append((fn, prefix + name + '/', package, only_in_packages))
+            elif package or not only_in_packages:
+                # is a file
+                bad_name = False
+                for pattern in exclude:
+                    if (fnmatchcase(name, pattern)
+                        or fn.lower() == pattern.lower()):
+                        bad_name = True
+                        print >> sys.stderr, (
+                            "File %s ignored by pattern %s"
+                            % (fn, pattern))
+                        break
+                if bad_name:
+                    continue
+                out.setdefault(package, []).append(prefix+name)
+    return out
+
+execfile(os.path.join("toscawidgets", "widgets", "dynforms", "release.py"))
+
+setup(
+    name=__PROJECT__,
+    version=__VERSION__,
+    description=__DESCRIPTION__,
+    author=__AUTHOR__,
+    author_email=__EMAIL__,
+    url=__URL__,
+    install_requires=[
+        "ToscaWidgets",
+        ## Add other requirements here
+        # "Genshi",
+        ],
+    packages=find_packages(exclude=['ez_setup', 'tests']),
+    namespace_packages = ['toscawidgets.widgets'],
+    zip_safe=False,
+    include_package_data=True,
+    test_suite = 'nose.collector',
+    package_data= find_package_data('toscawidgets.widgets.dynforms'),
+    entry_points="""
+        [toscawidgets.widgets]
+        # Use 'widgets' to point to the module where widgets should be imported
+        # from to register in the widget browser
+        widgets = toscawidgets.widgets.dynforms
+        # Use 'samples' to point to the module where widget examples
+        # should be imported from to register in the widget browser
+        samples = toscawidgets.widgets.dynforms.samples
+        # Use 'resources' to point to the module where resources
+        # should be imported from to register in the widget browser
+        #resources = toscawidgets.widgets.dynforms.resources
+    """,
+    keywords = [
+        'toscawidgets.widgets',
+    ],
+    classifiers = [
+        'Development Status :: 3 - Alpha',
+        'Environment :: Web Environment',
+        'Environment :: Web Environment :: ToscaWidgets',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: Software Development :: Widget Sets',
+        'Intended Audience :: Developers',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+    ],
+)

tests/__init__.py

Empty file added.
+from toscawidgets.testutil import WidgetTestCase
+from toscawidgets.widgets.dynforms import *
+
+class TestWidget(WidgetTestCase):
+    # place your widget at the TestWidget attribute
+    TestWidget = Dynforms
+    # Initilization args. go here 
+    widget_kw = {}
+
+    def test_render(self):
+        # Asserts 'foo' and 'test' (the test widget's id) appear in rendered 
+        # string when 'foo' is passed as value to render
+        self.assertInOutput(['foo', 'test'], "foo")
+        # Asserts 'ohlalala' does not appear in rendered string when render 
+        # is called without args
+        self.assertNotInOutput(['ohlalala'])

toscawidgets/__init__.py

+__import__('pkg_resources').declare_namespace(__name__)

toscawidgets/__init__.pyc

Binary file added.

toscawidgets/widgets/__init__.py

+__import__('pkg_resources').declare_namespace(__name__)

toscawidgets/widgets/__init__.pyc

Binary file added.

toscawidgets/widgets/dynforms/__init__.py

+from widgets import *
+from prochash import proc_hash

toscawidgets/widgets/dynforms/__init__.pyc

Binary file added.

toscawidgets/widgets/dynforms/prochash.py

+from sqlalchemy.exceptions import ArgumentError
+from sqlalchemy.orm import object_session, object_mapper
+import sqlalchemy
+
+def proc_hash(obj, data, mapper=None):
+    """Update a mapped class with data from a Python nested hash/list structure."""
+
+    if not mapper:
+        mapper = object_mapper(obj)
+    session = object_session(obj)
+
+    for col in mapper.mapped_table.c:
+        if not col.primary_key and data.has_key(col.name):
+            setattr(obj, col.name, data[col.name])
+    
+    xx = [(a,b) for a,b in getattr(obj.mapper, '_Mapper__props').items() if isinstance(b, sqlalchemy.orm.properties.PropertyLoader)]    
+    for rname,rel in xx:
+        if data.has_key(rname) and data[rname] is not None:
+            #pkey = mapper._pks_by_table[mapper.mapped_table]
+            pkey = [c for c in mapper.mapped_table.columns if c.primary_key]
+            lookup = {}
+            dbdata = getattr(obj, rname)
+            for subobj in dbdata:
+                lookup[tuple(getattr(subobj, c.name) for c in pkey)] = subobj
+
+            for row in data[rname]:
+                # if any primary key columns are missing or None, create a new object
+                if [1 for c in pkey if not row.get(c.name)]:
+                    subobj = rel.mapper.class_()
+                    dbdata.append(subobj)
+                else:
+                    subobj = lookup.pop(tuple(row[c.name] for c in pkey), None)
+                    # if the row isn't found, this could be an attempted parameter tampering attack
+                    if not subobj:
+                        raise ArgumentError('%s row not found in database: %s' % (rname, str(row)))
+                proc_hash(subobj, row, rel.mapper)
+
+            for delobj in lookup.itervalues():
+                session.delete(delobj)

toscawidgets/widgets/dynforms/prochash.pyc

Binary file added.

toscawidgets/widgets/dynforms/release.py

+# Metadata for the widget EGG. Should leave it for the (unimplemented) widget
+# tracker to track it
+
+__PROJECT__ = 'tw.dynforms'
+__DESCRIPTION__ = 'Widgets for dynamic forms, useful for intranet applications'
+__URL__ = 'http://pajhome.org.uk/'
+__VERSION__ = '0.1a0'
+__AUTHOR__ = 'Paul Johnston'
+__EMAIL__ = 'paj@pajhome.org.uk'
+__COPYRIGHT__ = "Copyright 2008 Paul Johnston"
+__LICENSE__ = ''

toscawidgets/widgets/dynforms/samples.py

+# Here you can create samples of your widgets by providing default parameters,
+# inserting them in a container widget, mixing them with other widgets, etc...
+# These samples will appear in the (unimplemented yet) widget browser.

toscawidgets/widgets/dynforms/static/ajax_lookup.js

+/**
+ * JavaScript to support Ajax lookups
+ **/
+var popup_data  = null;
+var popup_node  = null;
+var popup_elem  = null;
+var proc_req    = null;
+var proc_node   = null;
+var onfocus_val = null;
+
+/**
+ * Find a child node with a given suffix as its name
+ **/
+function find_node(node, suffix)
+{
+    for(var n = node.parentNode.firstChild; n; n = n.nextSibling)
+        if(n.id && n.id.lastIndexOf(suffix, n.id.length - suffix.length)
+                                                == n.id.length - suffix.length)
+            return n;
+}
+
+function find_node2(node, suffix)
+{
+    for(var m = node.parentNode.parentNode.firstChild; m; m = m.nextSibling)
+    {
+        for(var n = m.firstChild; n; n = n.nextSibling)
+                for(var o = n.firstChild; o; o = o.nextSibling)
+                    if(o.id && o.id.lastIndexOf(suffix, o.id.length - suffix.length)
+                                                == o.id.length - suffix.length)
+                        return o;
+    }
+}
+
+
+/**
+ * When user clicks "view" popup a new window with contact details
+ **/
+function view_contact(self)
+{
+    var staffid = find_node(self, '.id').value;
+    var url = "../pub/people?staffid=" + staffid;
+    window.open(url, 'view_contact', 'toolbar=0,scrollbars=0,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=400');
+}
+
+/**
+ * When the entry box gets focus, clear all styles
+ **/
+function entry_onfocus(self)
+{
+    if(find_node(self, '.id').value)
+        onfocus_val = self.value;
+    else
+        onfocus_val = 0;
+    set_style(self, 0);
+}
+
+/**
+ * When the entry box loses focus, validate using the server
+ **/
+function entry_onblur(self, evt, ajaxurl)
+{
+    if(self.value != '')
+    {
+        has_changed = 1;
+    }
+    if(onfocus_val && self.value == onfocus_val)
+    {
+        set_style(self, 1);
+        onfocus_val = 0;
+    }
+    else
+    {
+        find_node(self, '.id').value = '';
+        if(self.value != '')
+            check_contact(self, ajaxurl);
+    }
+}
+
+/**
+ * Make the Ajax request to the server - asynchronous
+ * If a request is already in progress, do nothing (but make entry box red)
+ **/
+function check_contact(node, ajaxurl)
+{
+    if(proc_req)
+    {
+        set_style(node, 2);
+        alert('A search is already in progress; please let it complete before making another.');
+        return;
+    }
+    set_style(node, 3);
+    proc_node = node;
+    proc_req = newXMLHttpRequest();
+    proc_req.onreadystatechange = process_response;
+    proc_req.open("POST", ajaxurl, true);
+    proc_req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
+    proc_req.send("search=" + escape(find_node(node, '.entry').value));
+}
+
+/**
+ * This function is invoked when the response is ready
+ **/
+function process_response()
+{
+    if(proc_req.readyState != 4) return;
+    if(proc_req.status != 200)
+    {
+        set_style(proc_node, 2);
+        proc_req = null;
+        proc_node = null;
+        alert('Server error processing request');
+        return;
+    }
+    var data = eval('x=' + proc_req.responseText);
+
+    if(data['status'] != 'Successful')
+    {
+        set_style(proc_node, 2);
+        alert(data['status']);
+    }
+    else if(data['data'].length == 0)
+        set_style(proc_node, 2);
+    else if(data['data'].length == 1)
+    {
+        var data = data['data'][0];
+        find_node(proc_node, '.entry').value = data['value'];
+        find_node(proc_node, '.id').value = data['id'];
+        set_style(proc_node, 1);
+    }
+    else if(popup_data)
+    {
+        set_style(proc_node, 2);
+        alert('Please select a contact from the open popup before performing another search.');
+    }
+    else
+    {
+        popup_node = proc_node;
+        popup_data = data;
+        raise_popup();
+    }
+
+    proc_req = null;
+    proc_node = null;
+}
+
+/**
+ * Set the style of a contact row
+ **/
+function set_style(node, num)
+{
+    var entry = find_node(node, '.entry');
+    var view  = find_node(node, '.link');
+
+    if(num == 0) /* editing */
+    {
+        entry.disabled = false;
+        entry.style.backgroundColor = 'white';
+        entry.style.textDecoration = '';
+        view.style.display = 'none';
+    }
+    if(num == 1) /* matched */
+    {
+        entry.disabled = false;
+        entry.style.backgroundColor = 'white';
+        entry.style.textDecoration = 'underline';
+        view.style.display = 'inline';
+    }
+    if(num == 2) /* problem */
+    {
+        entry.disabled = false;
+        entry.style.backgroundColor = 'red';
+        entry.style.textDecoration = '';
+        view.style.display = 'none';
+    }
+    if(num == 3) /* working */
+    {
+        entry.disabled = true;
+        view.style.display = 'none';
+    }
+}
+
+/**
+ * Raise the popup layer
+ **/
+function raise_popup()
+{
+    var html = "<table class='padded popup' onclick='event.cancelBubble = true'>";
+    for(var i = 0; i < popup_data['data'].length; i++)
+    {
+        var d = popup_data['data'][i];
+        html += "<tr><td><a href='pick' onclick='pick_contact(" + i + "); return false;'>"
+                 +    d['id'] + "</a></td><td>" + d['value'] + "</td></tr>";
+    }
+    html += "<tr><td><a href='cancel' onclick='cancel_popup(); return false;'>Cancel</a></td></tr></table>";
+
+    popup_elem = document.createElement('TR');
+    popup_elem.appendChild(document.createElement('TD'));
+    popup_elem.firstChild.colSpan = 5;
+    popup_elem.firstChild.innerHTML = html;
+    var tr_node = popup_node.parentNode.parentNode.parentNode;
+    tr_node.parentNode.insertBefore(popup_elem, tr_node.nextSibling);
+}
+
+/**
+ * When a user clicks a contact to select it, do the necessary.
+ **/
+function pick_contact(i)
+{
+    var data = popup_data['data'][i];
+    find_node(popup_node, '.entry').value = data['value'];
+    find_node(popup_node, '.id').value = data['id'];
+    set_style(popup_node, 1);
+    hide_popup();
+}
+
+/**
+ * Hide the popup layer
+ **/
+function hide_popup()
+{
+    popup_elem.parentNode.removeChild(popup_elem);
+    popup_data = null;
+    popup_node = null;
+    popup_elem = null;
+}
+
+/**
+ * When the document is clicked, if the popup layer is open, hide it.
+ * Also, set the current contact entry box to "problem".
+ **/
+function cancel_popup()
+{
+    set_style(popup_node, 2);
+    hide_popup();
+}
+
+/**
+ * Create an XMLHttpRequest object portably (both IE and Mozilla)
+ **/
+function newXMLHttpRequest()
+{
+    if(window.ActiveXObject)
+        return new ActiveXObject("Microsoft.XMLHTTP");
+    return new XMLHttpRequest();
+}

toscawidgets/widgets/dynforms/static/del.png

Added
New image

toscawidgets/widgets/dynforms/static/growing.js

+function mini_xpath(elem, path)
+{
+    var seek, nextpath, ret = new Array();
+
+    if(path.indexOf('/') == -1)
+        seek = path
+    else
+    {
+        seek = path.split('/')[0];
+        nextpath = path.substring(path.indexOf('/') + 1);
+    }
+
+    var seekl = seek.indexOf('|') == -1 ? [seek] : seek.split('|');
+    for(var node = elem.firstChild; node; node = node.nextSibling)
+    {
+        for(var i = 0; i < seekl.length; i++)
+            if(node.tagName && node.tagName == seekl[i])
+                ret = ret.concat(nextpath ? mini_xpath(node, nextpath) : [node]);
+             }
+    return ret;
+}
+
+
+/**
+ * Add a new row to a growable form:
+ *  Clone existing row
+ *  Update ID and name attributes
+ *  Blank data, including validation messages
+ *  Remove onchange events on old row
+ **/
+function grow_form(ctrl)
+{
+    var old_elem = ctrl.parentNode.parentNode;
+    var elem = old_elem.cloneNode(true);
+
+    var old_prefix = ctrl.name.substr(0, ctrl.name.lastIndexOf('.') + 1);
+    var new_prefix = old_prefix.replace(/(\d+)\.$/,
+                    function(m) { return (parseInt(m)+1) + '.'; });
+    var new_id_prefix = new_prefix.replace(/[-.]/g, '_');
+
+    var x = mini_xpath(elem, 'TD/INPUT|SELECT').concat(mini_xpath(elem, 'TD/SPAN/INPUT'));
+    for(i = 0; i < x.length; i++)
+    {
+        var suffix = x[i].name.substr(old_prefix.length);
+        x[i].name = new_prefix + suffix;
+        x[i].id = new_id_prefix + suffix;
+        x[i].value = '';
+        x[i].checked = 0;
+        x[i].style.backgroundColor = 'white';
+    }
+
+    var x = mini_xpath(old_elem, 'TD/INPUT|SELECT').concat(mini_xpath(elem, 'TD/SPAN/INPUT'));
+    for(i = 0; i < x.length; i++)
+        x[i].onchange = null;
+
+    old_elem.parentNode.appendChild(elem)
+}
+
+
+/**
+ * Function to delete fields, with undo
+ **/
+var undo = {};
+function del_fields(ctrl)
+{
+    has_changed = 1;
+    var x = mini_xpath(ctrl.parentNode.parentNode, 'TD/INPUT|SELECT').concat(mini_xpath(ctrl.parentNode.parentNode, 'TD/SPAN/INPUT'));
+    var any_set = 0;
+    if(ctrl.src.indexOf('del.png') > 0)
+    {
+        undo[ctrl.id] = Array();
+        for(var i = 0; i < x.length; i++)
+        {
+            if(x[i].type == 'checkbox')
+            {
+                    undo[ctrl.id][i] = x[i].checked;
+                    if(x[i].checked) any_set = 1;
+                    x[i].checked = 0;
+            }
+            else
+            {
+                    undo[ctrl.id][i] = x[i].value;
+                    if(x[i].value != '') any_set = 1;
+                    x[i].value = '';
+                    x[i].style.backgroundColor = 'white';
+            }
+        }
+        if(any_set) ctrl.src = '/toscawidgets/resources/toscawidgets.widgets.dynforms.widgets/static/undo.png';
+    }
+    else
+    {
+        for(var i = 0; i < x.length; i++)
+        {
+            if(x[i].type == 'checkbox')
+            {
+                    x[i].checked = undo[ctrl.id][i];
+            }
+            else
+            {
+                    x[i].value = undo[ctrl.id][i];
+            }
+        }
+        ctrl.src = '/toscawidgets/resources/toscawidgets.widgets.dynforms.widgets/static/del.png';
+    }
+}
+
+function get_all_nodes(elem)
+{
+    var ret = [elem];
+    for(var node = elem.firstChild; node; node = node.nextSibling)
+        ret = ret.concat(get_all_nodes(node))
+    return ret;
+}
+
+function add_section(button, desc)
+{    
+    var idprefix = button.parentNode.id;
+    var nameprefix = idprefix.substring(idprefix.indexOf('_')+1, 100); // TBD: this won't handle all cases correctly
+
+    // figure out next number in sequence
+    var node = button.parentNode.firstChild;
+    while(node)
+    {
+        if(node.id && node.id.indexOf(idprefix + '_rep-') == 0)
+            lastnode = node;
+        node = node.nextSibling;
+    }
+    var number = parseInt(lastnode.id.substr(idprefix.length + 5)) + 1;
+    
+    // clone and update
+    var old_elem = document.getElementById(idprefix + '_spare');
+    var elem = old_elem.cloneNode(true);
+    var id_stemlen = idprefix.length + 6;
+    var name_stemlen = nameprefix.length + 6;
+    
+    var new_name_prefix = nameprefix + '.rep-' + number;
+    var new_id_prefix = idprefix + '_rep-' + number;
+
+    var x = get_all_nodes(elem)
+    for(i = 0; i < x.length; i++)
+    {
+        /* if(x[i].tagName && x[i].tagName == 'H2' && x[i].innerHTML.match(new RegExp("^" + desc)))
+            x[i].innerHTML = desc + ' ' + (number+1); */
+        if(x[i].name)
+            x[i].name = new_name_prefix + x[i].name.substr(name_stemlen);
+        if(x[i].id)
+            x[i].id = new_id_prefix + x[i].id.substr(id_stemlen);
+    }
+    elem.id = new_id_prefix;
+    button.parentNode.insertBefore(elem, lastnode.nextSibling);
+}

toscawidgets/widgets/dynforms/static/hiding.js

+function addLoadEvent(func) {
+    var oldonload = window.onload;
+    if (typeof window.onload != 'function') {
+        window.onload = func;
+    } else {
+        window.onload = function() {
+            oldonload();
+            func();
+        }
+    }
+}
+
+function hssf_change(thing, mapping)
+{
+    var cont = document.getElementById(thing.id+'.container');
+    var visible = cont ? cont.style.display != 'none' : 1;
+    var stem = thing.id.substr(0, thing.id.lastIndexOf('_')+1);
+    var a, b;
+    for(a in mapping)
+        for(b in mapping[a])
+        {
+            try {
+            document.getElementById(stem+mapping[a][b]+'.container').style.display = (visible && (a == thing.value)) ? '' : 'none';
+            } catch(e) { alert('Missing control: ' + stem + mapping[a][b] + '.container'); }
+            var x = document.getElementById(stem+mapping[a][b]).onchange;
+            if(x) x();
+        }
+}
+
+function sel_link_change(thing)
+{
+    var visible = thing.style.display != 'none';    
+    document.getElementById(thing.id + '.view').style.display = visible && thing.value ? '' : 'none';
+}
+
+function do_popup(thing, linkurl)
+{
+    var value = document.getElementById(thing.id.substr(0, thing.id.length-5)).value;
+    window.open(linkurl.replace(/\$/, value));
+}
+
+function do_popup2(thing, linkurl)
+{
+    var value = document.getElementById(thing.id.substr(0, thing.id.length-5) + '.id').value;
+    window.open(linkurl.replace(/\$/, value));
+}
+
+
+function find_nodes(path)
+{
+    var ret = new Array();
+    var seekl = path.indexOf('|') == -1 ? [path] : path.split('|');
+    for(var i = 0; i < seekl.length; i++)
+    {
+        var nodes = document.getElementsByTagName(seekl[i]);
+        for(var j = 0; j < nodes.length; j++)
+            ret = ret.concat([nodes[j]]);
+    }
+    return ret;
+}
+
+function is_hidden(node)
+{
+    while(node.tagName != 'BODY')
+    {
+        if(node.style.display == 'none')
+            return 1;
+        node = node.parentNode;
+    }
+    return 0;
+}
+
+function blank_invisible()
+{
+    var x = find_nodes('INPUT|SELECT|TEXTAREA');
+    for(var i = 0; i < x.length; i++)
+    {
+        if(is_hidden(x[i]))
+            x[i].value = '';
+    }
+}

toscawidgets/widgets/dynforms/static/undo.png

Added
New image

toscawidgets/widgets/dynforms/templates/__init__.py

Empty file added.

toscawidgets/widgets/dynforms/templates/__init__.pyc

Binary file added.

toscawidgets/widgets/dynforms/templates/contact_field.html

+<span xmlns:py="http://genshi.edgewall.org/">
+    <input
+        type="text"
+        id="${id}.entry"
+        value="${visvalue}"
+        onblur="entry_onblur(this, event, '${ajaxurl}')"
+        onfocus="entry_onfocus(this)"
+        style="text-decoration: underline"/>
+    <input
+        type="hidden"
+        name="${name}"
+        id="${id}.id"
+        value="${value}"/>
+    <a href="view_contact"
+        onclick="do_popup2(this, '$link'); return false"
+        id="${id}.link"
+        style="display:${showview}">${view_text}</a>
+</span>

toscawidgets/widgets/dynforms/templates/delundo.html

+<input xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/"
+    type="image"
+    id="${id}"
+    class="${css_class}"
+    py:attrs="attrs"
+    src="/toscawidgets/resources/toscawidgets.widgets.dynforms.widgets/static/del.png"
+    onclick="del_fields(this); return false;"
+/>

toscawidgets/widgets/dynforms/templates/growablerepeater.html

+<span xmlns:py="http://genshi.edgewall.org/" id="${id}" class="${name}">
+    ${children['rep'].display(value, **args_for(children['rep']))}
+    <div style="display:none">${display_child('spare')}</div>
+    ${display_child('add')}
+</span>

toscawidgets/widgets/dynforms/templates/hiding_select_field.html

+<span>
+<select xmlns="http://www.w3.org/1999/xhtml"
+        xmlns:py="http://genshi.edgewall.org/"
+        name="${name}"
+        class="${css_class}"
+        id="${id}"
+        py:attrs="attrs"
+        onchange="hssf_change(document.getElementById('$id'), $mapping)">
+    <optgroup py:for="group, options in grouped_options"
+              py:strip="not group"
+              label="${group}" >
+        <option py:for="value, desc, attrs in options"
+                py:attrs="attrs"
+                py:content="desc"
+                value="${value}" />
+    </optgroup>
+</select>
+<script>addLoadEvent(function() { hssf_change(document.getElementById('${id}'), $mapping) })</script>
+</span>

toscawidgets/widgets/dynforms/templates/ossf.html

+<span xmlns:py="http://genshi.edgewall.org/">
+    ${children['select'].display(value, **args_for(children['select']))}
+    <span id="${children['other'].id}.container">$specify_text ${children['other'].display('', **args_for(children['other']))}</span>
+</span>

toscawidgets/widgets/dynforms/templates/smartlist.html

+<div xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/">
+
+<form name="smartlist" method="post" autocomplete="off">
+<p>
+    <py:if test="search_cols">
+        Keyword search: ${src_text(value_for(src_text))}
+        ${src_search()}
+        <py:if test="value.get('src_text')">${src_clear()}</py:if>
+        &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
+    </py:if>
+
+    <py:for each="c in checkboxes">
+        ${c(value_for(c), **disabled)} $c.label &nbsp; &nbsp; &nbsp;
+    </py:for>
+</p>
+
+<table class="alltsrs">
+    <tr>
+        <th py:for="c,l in columns">$l</th>
+    </tr>
+    <tr>
+        <td py:for="c,l in columns">
+            <py:if test="dropdowns.has_key(c)">
+                <py:if test="dfilt.has_key(c)">${dropdowns[c](value_for(dropdowns[c]), options=dfilt[c], **disabled)}</py:if>
+                <py:if test="not dfilt.has_key(c)">${dropdowns[c](value_for(dropdowns[c]), **disabled)}</py:if>
+            </py:if>
+        </td>
+    </tr>
+    <tr py:for="i,r in enumerate(data)" class="${(i%2 == 0) and 'altrow'}">
+        <td py:for="c,l in columns">${getattr(r, c)}</td>
+    </tr>
+    <tr py:if="not data">
+        <td colspan="${len(columns)}" style="text-align:center" class="altrow">$blank_msg</td>
+    </tr>
+</table>
+</form>
+
+</div>

toscawidgets/widgets/dynforms/templates/table_field_set.html

+<table  xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      border="0" cellspacing="0" cellpadding="2" py:attrs="table_attrs" class="autogrow ${css_class}">
+    <tr py:if="dotitle"><th py:for="c in children['grow'].widget.children" py:if="c.name != 'id'" title="$c.help_text">${c.label_text}</th></tr>
+    ${children['grow'].display(value, **args_for(children['grow']))}
+</table>

toscawidgets/widgets/dynforms/templates/table_form.html

+<form xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+    id="${id}"
+    name="${name}"
+    action="${action}"
+    method="${method}"
+    class="${css_class} autogrow"
+    py:attrs="attrs" >
+    <div>
+        <div py:for="field in ihidden_fields"
+             py:with="error=error_for(field)"
+             py:strip="True">
+            <span py:replace="field.display(value_for(field), **args_for(field))" />
+            <span py:if="show_children_errors and error and not field.show_error"
+                  class="fielderror" py:content="error" />
+        </div>
+    </div>
+    <table border="0" cellspacing="0" cellpadding="2" py:attrs="table_attrs">
+        <tr py:if="dotitle"><th py:for="c in children['grow'].widget.children">${c.label_text}</th></tr>
+        ${children['grow'].display(value_for(children['grow']), **args_for(children['grow']))}
+    </table>
+
+    <p>${children['submit'].display()}</p>
+</form>
+

toscawidgets/widgets/dynforms/templates/trfieldset.html

+<tr xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/"
+    id="${id}"
+    class="${css_class}"
+    py:attrs="attrs"
+>
+    <py:for each="field in ifields">
+        <td title="${field.help_text}">${field.display(value_for(field), **fred(field))}</td>
+    </py:for>
+    <td><py:for each="field in ihidden_fields">
+        ${field.display(value_for(field), **fred(field))}
+    </py:for></td>
+</tr>

toscawidgets/widgets/dynforms/widgets.py

+import toscawidgets.api as tw, toscawidgets.widgets.forms as twf
+import sqlalchemy as sa, formencode as fe, turbojson
+
+__all__ = ['SmartList', 'WriteOnlyTextField', 'strip_wo_markers',
+           'HidingSingleSelectField', 'IntNull', 'load_options',
+           'GrowableTableFieldSet', 'GrowableTableForm',
+           'OtherSingleSelectField', 'LinkSingleSelectField',
+           'AjaxLookupField', 'GrowableRepeater']
+
+#--
+# Smart, dynamically filterable list
+#--
+class SmartList(twf.Form):
+    params = \
+    {
+        'datasrc':      'Model object to use as data source',
+        'columns':      'List of (name, label) pairs to display',
+        'search_cols':  'List of column names to include in keyword search',
+        'options':      'Checkbox options; a list of (label, value, condition) tuples',
+        'data_filter':  'List of column names to have autofilter dropdowns',
+        'code_filter':  'Columns to have codified dropdown filters',
+        'order_by':     'Condition for the order_by clause of the query',
+        'blank_msg':    'Text to display if there are no results',
+        'conds':        'Enforced filter conds',
+
+        # TBD: these shouldn't really be parameters; just need to be passed into template somehow
+        'dropdowns': 'x',
+        'checkboxes': 'x',
+        'src_text':'x',
+        'src_search':'x',
+        'src_clear':'x',
+        'dfilt':'x',
+        'data':'x',
+        'disabled':'x',
+    }
+    template = "genshi:toscawidgets.widgets.dynforms.templates.smartlist"
+
+    blank_msg = "(nothing to show)"
+    search_cols = []
+    options = []
+    data_filter = []
+    code_filter = {}
+    conds = []
+
+    def __init__(self, *args, **kwargs):
+        super(SmartList, self).__init__(*args, **kwargs)
+
+        self.dropdowns = {}
+        for c,x in self.columns:
+            if c in self.data_filter:
+                self.dropdowns[c] = twf.SingleSelectField(c, self, attrs={'onchange':'smartlist.submit()'})
+            elif self.code_filter.has_key(c):
+                self.dropdowns[c] = twf.SingleSelectField(c, self, options = [x[0] for x in self.code_filter[c]], attrs={'onchange':'smartlist.submit()'})
+
+        self.checkboxes = []
+        for i,(l,t,c) in enumerate(self.options):
+            self.checkboxes.append(twf.CheckBox('cb_%d' % i, self, label=l, attrs={'onclick':'smartlist.submit()'}))
+
+        if self.search_cols:
+            self.src_text = twf.TextField('src_text', self)
+            self.src_search = twf.SubmitButton('src_search', self, default='Search')
+            self.src_clear = twf.SubmitButton('src_clear', self, default='Clear', attrs={'onclick':"document.getElementById('%s').value=''" % self.src_text.id})
+
+
+    def update_params(self, params):
+
+        # generate appropriate query constraints from filters
+        conds = list(self.conds)
+        src = params['value'].get('src_text')
+
+        if src:
+            conds.append(sa.or_(*[self.datasrc.c[c].like('%'+src+'%') for c in self.search_cols]))
+
+        else:
+            for q in self.code_filter:
+                v = params['value'].get(q)
+                z = [y for x,y in self.code_filter[q] if x == v]
+                if z and z[0]:
+                    conds.append(z[0])
+            for i,(l,t,c) in enumerate(self.options):
+                v = bool(params['value'].get('cb_%d' % i))
+                if v == t:
+                    conds.append(c)
+
+        # load data
+        for i in range(len(conds)):
+            if hasattr(conds[i], '__call__'):
+                conds[i] = conds[i]()
+        params['data'] = self.datasrc.query.filter(sa.and_(*conds)).order_by(self.order_by)
+
+        if not src:
+            # apply data filters
+            out = []
+            for x in params['data']:
+                for q in self.data_filter:
+                    if params['value'].get(q) not in (None, 'All', str(getattr(x, q))):
+                        break
+                else:
+                    out.append(x)
+            params['data'] = out
+
+        # Generate contents for data dropdowns
+        dfilt = dict((d, {}) for d in self.data_filter)
+        for row in params['data']:
+            for d in self.data_filter:
+                val = getattr(row, d)
+                if val:
+                    dfilt[d][str(val)] = 1
+        params['dfilt'] = dict((d, dfilt[d].keys()) for d in dfilt)
+        for d in self.data_filter:
+            v = params['value'].get(d)
+            if v and v != 'All' and v not in params['dfilt'][d]:
+                params['dfilt'][d].append(v)
+        for x in params['dfilt'].values():
+            x.sort()
+            x.insert(0, 'All')
+
+        # disable dropdowns when search active
+        params['disabled'] = src and {'disabled': True} or {}
+
+        super(SmartList, self).update_params(params)
+
+
+#--
+# Write-only text fields; the server never discloses the content
+#--
+class WriteOnlyMarker(object):
+    pass
+
+class WriteOnlyValidator(fe.validators.FancyValidator):
+    def __init__(self, token, *args, **kw):
+        super(WriteOnlyValidator, self).__init__(*args, **kw)
+        self.token = token
+    def to_python(self, value, state):
+        return value == self.token and WriteOnlyMarker() or value
+
+class WriteOnlyTextField(twf.TextField):
+    """A text field that is write-only and never reveals database content"""
+    params = {'token': 'Text that is displayed instead of the data. This can only be specified at widget creation, not at display time.'}
+    token = '(supplied)'
+    def __init__(self, *args, **kw):
+        super(WriteOnlyTextField, self).__init__(*args, **kw)
+        self.validator = WriteOnlyValidator(self.token)
+    def adjust_value(self, value, validator=None):
+        return value and self.token or value
+
+def strip_wo_markers(val):
+    if isinstance(val, list):
+        for v in val:
+            strip_wo_markers(v)
+    elif isinstance(val, dict):
+        for k,v in val.items():
+            if isinstance(v, WriteOnlyMarker):
+                del val[k]
+    return val
+
+
+#--
+#
+#--
+class IntNull(fe.validators.Int):
+    def _to_python(self, value, state):
+        if value == '':
+            return None
+        else:
+            return super(IntNull, self)._to_python(value, state)
+
+def load_options(datasrc, code=None, extra=[('', '')]):
+    if hasattr(datasrc, 'query'): # TBD: figure a different test, this is a hack
+        datasrc = datasrc.query
+    data = datasrc.all()
+    if data and not code:
+        code = [c for c in data[0].c if c.primary_key][0].key
+    options = [(getattr(x, code), str(x)) for x in datasrc.all()]
+    options.sort(key = lambda x: x[1]) # TBD: remove this
+    return extra + options
+
+
+#--
+# Growable forms
+#--
+class DelUndo(twf.FormField):
+    template = "toscawidgets.widgets.dynforms.templates.delundo"
+    validator = None
+    label_text = ''
+
+class TrFieldSet(twf.FieldSet):
+    template = "toscawidgets.widgets.dynforms.templates.trfieldset"
+
+    def update_params(self, d):
+        super(TrFieldSet, self).update_params(d)
+        is_extra = d['isextra']
+        q = d['args_for']
+        def fred(s):
+            l = q(s)
+            if is_extra and 'delundo' not in s.name:
+                l['attrs'] = {'onchange': 'grow_form(this)', 'class': s.css_class}
+                if 'getsreport' in s.name:
+                    del l['attrs']['class']
+            return l
+        d['fred'] = fred
+
+    def post_init(self, *args, **kwargs):
+        super(TrFieldSet, self).post_init(*args, **kwargs)
+        self.validator.if_missing = None
+
+class StripBlanks(fe.ForEach):
+    def any_content(self, val):
+        if type(val) == list:
+            for v in val:
+                if self.any_content(v):
+                    return True
+            return False
+        elif type(val) == dict:
+            for k in val:
+                if k == 'id':
+                    continue
+                if self.any_content(val[k]):
+                    return True
+            return False
+        else:
+            return bool(val)
+
+    def _to_python(self, value, state):
+        val = super(StripBlanks, self)._to_python(value, state)
+        return [v for v in val if self.any_content(v)]
+
+class StripGrow(fe.Schema):
+    def _to_python(self, value, state):
+        return super(StripGrow, self)._to_python(value, state).get('grow', [])
+
+class GrowableTableFieldSet(twf.FieldSet):
+    javascript = [tw.JSLink(modname=__name__, filename="static/growing.js")]
+    params = {
+        'colspan': '',
+        'dotitle': 'Whether to include a title row in the table',
+    }
+    colspan = 1
+    dotitle = True
+    template = 'toscawidgets.widgets.dynforms.templates.table_field_set'
+    validator = StripGrow
+
+    def __new__(cls, id=None, parent=None, children=[], **kw):
+        children.append(tw.Child(DelUndo, 'delundo'))
+        children.append(tw.Child(twf.HiddenField, 'id', validator=fe.validators.Int))
+        children = [tw.Child(StrippingFieldRepeater, 'grow', widget=TrFieldSet('row', children=children))]
+        return twf.FieldSet.__new__(cls, id, parent, children, **kw)
+
+class GrowableTableForm(GrowableTableFieldSet):
+    template = 'toscawidgets.widgets.dynforms.templates.table_form'
+
+class StrippingFieldRepeater(twf.FormFieldRepeater):
+    extra = 1
+    repetitions = 0
+    max_repetitions = 10 # TBD: recode toscawidgets to not need this
+    def post_init(self, *args, **kwargs):
+        self.validator = StripBlanks(self.children[0].validator)
+
+
+class StripRep(fe.Schema):
+    def _to_python(self, value, state):
+        return super(StripRep, self)._to_python(value, state).get('rep', [])
+
+class GrowableRepeater(twf.FieldSet):   
+    validator = StripRep
+    javascript = [tw.JSLink(modname=__name__, filename="static/growing.js")]
+    template = 'toscawidgets.widgets.dynforms.templates.growablerepeater'
+    params = ['button_text', 'widget']
+    button_text = 'Add'
+    button_text__doc = 'Text to use on "add" button'
+    widget__doc = 'Widget to repeat'
+
+    def __new__(cls, id=None, parent=None, children=[], widget=None, **kw):
+        children = [
+            tw.Child(twf.FormFieldRepeater, 'rep', widget=widget, extra=0, repetitions=1, max_repetitions=10),                        
+            tw.Child(widget, 'spare'),
+            tw.Child(twf.Button, 'add', default=cls.button_text, attrs={'onclick':"add_section(this, '')"}),
+        ]
+        return twf.FieldSet.__new__(cls, id, parent, children, **kw)
+
+
+#--
+# Fancy SingleSelectField derivatices
+#--
+class HidingSingleSelectField(twf.SingleSelectField):
+    template = 'genshi:toscawidgets.widgets.dynforms.templates.hiding_select_field'
+    javascript = [tw.JSLink(modname=__name__, filename='static/hiding.js')]
+    params = {
+        'mapping': 'Dict that maps selection values to visible controls'
+    }
+    def update_params(self, params):
+        params['mapping'] = turbojson.jsonify.encode(params.get('mapping', self.mapping))
+        super(HidingSingleSelectField, self).update_params(params)
+
+
+class LinkSingleSelectField(twf.SingleSelectField):
+    template = "toscawidgets.widgets.dynforms.templates.link_select_field"
+    javascript = [tw.JSLink(modname=__name__, filename='static/hiding.js')]
+    params = {
+        'link':         'Link target',
+        'view_text':    'Allows you to override the text string "view"',
+    }
+    view_text = 'View'
+
+
+class OtherChoiceValidator(fe.Schema):
+    select = IntNull()
+    other = fe.validators.String()
+
+    def __init__(self, dataobj, field, code, other_code, fixed_fields, *args, **kwargs):
+        super(OtherChoiceValidator, self).__init__(*args, **kwargs)
+        self.dataobj = dataobj
+        self.field = field
+        self.code = code
+        self.other_code = other_code
+        self.fixed_fields = fixed_fields
+
+    def _to_python(self, value, state):
+        val = super(OtherChoiceValidator, self)._to_python(value, state)
+        if val['select'] == self.other_code:
+            data = {self.field: value['other']}
+            data.update(self.fixed_fields)
+            obj = self.dataobj(**data)
+            sao.object_session(obj).flush([obj])
+            return getattr(obj, self.code)
+        else:
+            return val['select']
+
+
+class OtherSingleSelectField(twf.FormField):
+    template = "toscawidgets.widgets.dynforms.templates.ossf"
+
+    params = {
+        'datasrc':      'The SQLAlchemy data source to use',
+        'dataobj':      'The SQLAlchemy object to use',
+        'field':        'The field on the object to use for the "other" text',
+        'code':         'The field on the object that is the code',
+        'other_code':   'Allows you to override the code used for "other"',
+        'other_text':   'Allows you to override the text string "other"',
+        'specify_text': 'Allows you to override the text string "Please specify:"',        
+        'fixed_fields': 'Specify field values on newly created objects',
+    }
+    code = 'id'
+    other_code = 10000
+    other_text = 'Other'
+    specify_text = 'Please specify:'
+    fixed_fields = {}
+
+    children = [
+        tw.Child(HidingSingleSelectField, 'select'),
+        tw.Child(twf.TextField, 'other'),
+    ]
+
+    # This is needed to avoid the value being coerced to a dict
+    def adapt_value(self, value):
+        return value
+
+    def update_params(self, kw):
+        options = load_options(self.datasrc, self.code)
+        options.append((self.other_code, self.other_text))
+        kw['child_args'] = {'select': {
+            'options': options,
+            'mapping': {self.other_code: ['other']}
+        }}
+        return super(OtherSingleSelectField, self).update_params(kw)
+
+    def __init__(self, id, dataobj, field, datasrc=None, *args, **kw):
+        self.datasrc = datasrc and datasrc or dataobj
+        super(OtherSingleSelectField, self).__init__(id, *args, **kw)
+        self.validator = OtherChoiceValidator(dataobj, field,
+                kw.get('code', self.code),
+                kw.get('other_code', self.other_code),
+                kw.get('fixed_fields', self.fixed_fields))
+
+
+#--
+# Contact lookup
+#--
+class AjaxLookupField(twf.FormField):
+    "A text field that searches using AJAX"
+    javascript = [tw.JSLink(modname=__name__, filename="static/ajax_lookup.js"),
+                  tw.JSLink(modname=__name__, filename="static/hiding.js")] # just for hiding the view link
+    params = {
+        'attrs':    '',
+        'datasrc':  'SQLAlchemy data src',
+        'ajaxurl':  'URL of ajax responder',
+        'link':     'Link target',
+        'view_text':'Allows you to override the text string "view"',
+    }
+    attrs = {}
+    view_text = 'View'
+
+    template = "toscawidgets.widgets.dynforms.templates.contact_field"
+
+    def display(self, value, **params):
+        if value:
+            params['visvalue'] = str(self.datasrc.query.get(value))
+            params['showview'] = 'inline'
+        else:
+            params['visvalue'] = ''
+            params['showview'] = 'none'
+        return super(AjaxLookupField, self).display(value, **params)
+

toscawidgets/widgets/dynforms/widgets.pyc

Binary file added.

tw.dynforms.egg-info/PKG-INFO

+Metadata-Version: 1.0
+Name: tw.dynforms
+Version: 0.1a0dev
+Summary: Widgets for dynamic forms, useful for intranet applications
+Home-page: http://pajhome.org.uk/
+Author: Paul Johnston
+Author-email: paj@pajhome.org.uk
+License: UNKNOWN
+Description: UNKNOWN
+Keywords: toscawidgets.widgets
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Environment :: Web Environment
+Classifier: Environment :: Web Environment :: ToscaWidgets
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Widget Sets
+Classifier: Intended Audience :: Developers
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python

tw.dynforms.egg-info/SOURCES.txt

+setup.cfg
+setup.py
+toscawidgets/__init__.py
+toscawidgets/widgets/__init__.py
+toscawidgets/widgets/dynforms/__init__.py
+toscawidgets/widgets/dynforms/prochash.py
+toscawidgets/widgets/dynforms/release.py
+toscawidgets/widgets/dynforms/samples.py
+toscawidgets/widgets/dynforms/widgets.py
+toscawidgets/widgets/dynforms/templates/__init__.py
+tw.dynforms.egg-info/PKG-INFO
+tw.dynforms.egg-info/SOURCES.txt
+tw.dynforms.egg-info/dependency_links.txt
+tw.dynforms.egg-info/entry_points.txt
+tw.dynforms.egg-info/namespace_packages.txt
+tw.dynforms.egg-info/not-zip-safe
+tw.dynforms.egg-info/paster_plugins.txt
+tw.dynforms.egg-info/requires.txt
+tw.dynforms.egg-info/top_level.txt

tw.dynforms.egg-info/dependency_links.txt

+

tw.dynforms.egg-info/entry_points.txt

+
+        [toscawidgets.widgets]
+        # Use 'widgets' to point to the module where widgets should be imported
+        # from to register in the widget browser
+        widgets = toscawidgets.widgets.dynforms
+        # Use 'samples' to point to the module where widget examples
+        # should be imported from to register in the widget browser
+        samples = toscawidgets.widgets.dynforms.samples
+        # Use 'resources' to point to the module where resources
+        # should be imported from to register in the widget browser
+        #resources = toscawidgets.widgets.dynforms.resources
+    

tw.dynforms.egg-info/namespace_packages.txt

+toscawidgets.widgets

tw.dynforms.egg-info/not-zip-safe

+

tw.dynforms.egg-info/paster_plugins.txt

+PasteScript

tw.dynforms.egg-info/requires.txt

+ToscaWidgets

tw.dynforms.egg-info/top_level.txt

+toscawidgets