Author: croberts
Date: 2011-08-05 18:58:51 +0000 (Fri, 05 Aug 2011)
New Revision: 4907
Added:
trunk/cumin/python/cumin/grid/tags.py
trunk/cumin/python/cumin/grid/tags.strings
Modified:
trunk/cumin/model/cumin.xml
trunk/cumin/python/cumin/grid/pool.py
trunk/cumin/resources/app.css
trunk/wooly/python/wooly/forms.strings
trunk/wooly/resources/mootools.js
Log:
First cut of changes to bring wallaby tagging into the cumin UI. Highlights include: The
tags.py package that contains all of the classes required, additions to mootools.js to
provide a hybrid select box, changes to app.css to provide the required styling for the
new elements.
Modified: trunk/cumin/model/cumin.xml
===================================================================
--- trunk/cumin/model/cumin.xml 2011-08-04 16:40:02 UTC (rev 4906)
+++ trunk/cumin/model/cumin.xml 2011-08-05 18:58:51 UTC (rev 4907)
@@ -53,5 +53,13 @@
<property name="Usage" type="sstr"/>
<property name="Allowance" type="sstr"/>
</class>
+
+ <class name="Node" storage="none">
+ <property name="Name" type="sstr"/>
+ <property name="Tags" type="sstr"/>
+ <property name="Checkin" type="sstr"/>
+ </class>
+
+
</package>
</model>
Modified: trunk/cumin/python/cumin/grid/pool.py
===================================================================
--- trunk/cumin/python/cumin/grid/pool.py 2011-08-04 16:40:02 UTC (rev 4906)
+++ trunk/cumin/python/cumin/grid/pool.py 2011-08-05 18:58:51 UTC (rev 4907)
@@ -7,6 +7,7 @@
from cumin.objectframe import ObjectFrame, ObjectView
from cumin.stat import StatFlashChart, StatSet
from cumin.grid.dashboard import PoolDashboard
+from cumin.grid.tags import TagsEditor, TagsNodeEditTask, TagsFrame
from submission import SubmissionFrame, PoolSubmissionJoinSelector
from slot import SlotFrame
@@ -74,6 +75,9 @@
self.limit = LimitFrame(app, "limit")
self.add_mode(self.limit)
+
+ self.tag = TagsFrame(app, "tag")
+ self.add_mode(self.tag)
dashboard = PoolDashboard(app, "dashboard", self.object)
self.view.add_tab(dashboard)
@@ -97,6 +101,10 @@
self.edit_dynamic_quota = NegotiatorEditDynamicQuota(app, self)
self.quotas = QuotaSelector(app, "quotas", self.negotiator_attribute,
self)
self.view.add_tab(self.quotas)
+
+ self.edit_node_tags = TagsNodeEditTask(app, self)
+ config_editor = TagsEditor(app, "wallaby")
+ self.view.add_tab(config_editor)
self.limits = LimitSelector(app, "limits")
self.view.add_tab(self.limits)
Added: trunk/cumin/python/cumin/grid/tags.py
===================================================================
--- trunk/cumin/python/cumin/grid/tags.py (rev 0)
+++ trunk/cumin/python/cumin/grid/tags.py 2011-08-05 18:58:51 UTC (rev 4907)
@@ -0,0 +1,869 @@
+import logging
+
+from wooly import Widget, Attribute
+from wooly.util import StringCatalog, Writer
+from wooly.datatable import DataAdapterOptions, DataAdapterField, DataTableColumn,
DataTable, DataAdapter
+from wooly.widgets import RadioModeSet, WidgetSet
+from wooly.template import WidgetTemplate
+from wooly.forms import *
+from wooly.table import Table, TableColumn
+from wooly.tables import *
+
+from cumin.formats import fmt_link, fmt_datetime
+from cumin.objectselector import *
+from cumin.objectframe import ObjectFrameTaskForm, ObjectFrameTaskFeedbackForm,
ObjectFrameTask
+from cumin.parameters import RosemaryObjectParameter
+from cumin.task import TaskLink, Task, ObjectTaskForm
+from cumin.widgets import *
+
+from sage.wallaby.wallabyoperations import WallabyOperations, WBTypes
+from sage.util import *
+
+
+strings = StringCatalog(__file__)
+log = logging.getLogger("cumin.tags")
+
+class ResponseAdapter(ObjectQmfAdapter):
+ '''
+ This class is necessary to act as an adapter to take the response
+ that we get from wallaby and turn it into something that we can readily
+ display in a table format. Typically, a table will have an adapter assigned
+ to do the transformations necessary for display.
+ '''
+ def get_count(self, values):
+ ''' Returns the current count of rows for the table (after filtering
which is done in get_data() '''
+ data = self.get_data(values, None)
+ return len(data)
+
+ def get_data(self, values, options):
+ '''
+ Calls do_get_data to fetch the remote data, then calls filter,
+ sort and limit to sift through the data.
+ '''
+ data = self.do_get_data(values)
+ rows = self.process_results(data)
+
+ rows = self.filter_rows(rows, values)
+ if options:
+ rows = self.sort_rows(rows, options)
+ rows = self.limit_rows(rows, options)
+
+ return rows
+
+ def process_results(self, results):
+ ''' take the dict response from the call and return a list of lists
'''
+ records = list()
+
+ if results:
+ for i, result in enumerate(results):
+ row = self.process_record(i, results[i])
+ records.append(row)
+
+ return records
+
+ def filter_rows(self, rows, values):
+ '''
+ Looks for the field name and filter value in the 'values' parameter, does
a "contains" filtering on
+ each rows entry for the given field.
+ '''
+ filtered = list()
+ filter_field = values.keys()[0]
+ filter_text = values[filter_field].replace("%","")
+
+ for row in rows:
+ if filter_text.lower() in
row[self.fields_by_name[filter_field].index].lower():
+ filtered.append(row)
+ return filtered
+
+
+class TagsAdapter(ResponseAdapter):
+ '''
+ This class is meant to adapt the wallaby response for get_data(TAGS) into a format
appropriate
+ for displaying in the tags table
+ '''
+ def do_get_data(self, values):
+ data = []
+ try:
+ wallaby_tags = self.app.wallaby.get_data(WBTypes.TAGS)
+
+ for i, tag in enumerate(wallaby_tags):
+ data.append({'Tag':str(tag.name),
'Nodes':self.app.wallaby.get_node_names(tag)})
+
+ except Exception, e:
+ log.exception(e)
+
+ return data
+
+ def process_results(self, results):
+ """ Take the get_data response and process it for viewing by tags
"""
+
+ records = list()
+ if results:
+ for result in results:
+ records.append([result['Tag'], result['Nodes']])
+
+ return records
+
+class NodesAdapter(ResponseAdapter):
+ '''
+ This class is meant to adapt the wallaby response for get_data(NODES) into a format
appropriate
+ for displaying in the nodes table
+ '''
+ def process_record(self, key, record):
+ """ Take the get_data response and process it for viewing by nodes
"""
+ return
[record["Name"],record["Tags"],record["Checkin"]]
+
+ def do_get_data(self, values):
+ '''
+ Make the wallaby call and return a list of dicts that will be
+ formatted for the table by the process_record method
+ '''
+ wallaby_nodes = self.app.wallaby.get_data(WBTypes.NODES)
+ data = []
+ for i, node in enumerate(wallaby_nodes):
+ data.append({'Name':node.name,
'Tags':self.app.wallaby.get_tag_names(node),
'Checkin':node.last_checkin})
+
+ return data
+
+class ObjectSelectorTableNoCheckboxes(ObjectSelector):
+ '''
+ All of the ObjectSelector goodness, but without the checkboxes that come from the
default ObjectSelectorTable
+ '''
+ def __init__(self, app, name, cls):
+ super(ObjectSelectorTableNoCheckboxes, self).__init__(app, name, cls)
+
+ def create_table(self, app, name, cls):
+ ''' override the default to give us a plain ObjectTable rather than
and ObjectSelectorTable '''
+ return ObjectTable(app, name, cls)
+
+class TagsFrame(ObjectFrame):
+ def __init__(self, app, name):
+ cls = app.model.com_redhat_cumin_grid.Node
+ super(TagsFrame, self).__init__(app, name, cls)
+ self.set_tags = TagsNodeEditTask(app, self)
+ self.set_nodes = TagsTagEditTask(app, self)
+
+class TagInventory(ObjectSelectorTableNoCheckboxes):
+ def __init__(self, app, name):
+ cls = app.model.com_redhat_cumin_grid.Node
+ super(TagInventory, self).__init__(app, name, cls)
+ self.table.adapter = TagsAdapter(app, cls)
+
+ tag_add_task = AddTags(app)
+ link = TaskLink(app, "tag_add", tag_add_task)
+ self.links.add_child(link)
+
+ remove_node_tags_task = RemoveNodeTags(app)
+ link = TaskLink(app, "tag_remove", remove_node_tags_task)
+ self.links.add_child(link)
+
+ col = self.TagColumn(app, "tagcol", cls.Tags)
+ self.add_column(col)
+ self.add_search_filter(col)
+
+ def render_title(self, session):
+ return "Tags"
+
+ class TagColumn(ObjectTableColumn):
+ def render_cell_content(self, session, data):
+ tags = super(TagInventory.TagColumn, self).render_cell_content(session,
data)
+ node_list = data[1]
+ nodes = ""
+ if node_list:
+ for node in node_list:
+ nodes = nodes + "," + str(node)
+ nodes = nodes[1:]
+ #here we set some info that will be used in the display of the target form
+ self.frame.tag.set_nodes.form.node_name.set(session, nodes)
+ self.frame.tag.set_nodes.form.tags.set(session, tags)
+ href = self.frame.tag.set_nodes.get_href(session)
+ return fmt_link(href, tags)
+
+
+class NodeInventory(ObjectSelectorTableNoCheckboxes):
+ def __init__(self, app, name):
+ cls = app.model.com_redhat_cumin_grid.Node
+ super(NodeInventory, self).__init__(app, name, cls)
+ self.table.adapter = NodesAdapter(app, cls)
+
+# NOT SURE IF THIS FUNCTIONALITY IS NECESSARY, turned off for now
+# edit_node_tags_task = ChangeNodeTags(app)
+# link = TaskLink(app, "node_tag_edit", edit_node_tags_task)
+# self.links.add_child(link)
+
+ col = self.NodeColumn(app, "nodecol", cls.Name)
+ self.add_column(col)
+ self.add_search_filter(col)
+
+ col = self.TagsColumn(app, "tagcol", cls.Tags)
+ self.add_column(col)
+
+ col = self.CheckinColumn(app, "checkcol", cls.Checkin)
+ self.add_column(col)
+
+ self.update_enabled = False
+
+ def get_data_values(self, session):
+ values = super(NodeInventory, self).get_data_values(session)
+ values['session'] = session
+ return values
+
+ def render_title(self, session):
+ return "Nodes"
+
+ class CheckinColumn(ObjectTableColumn):
+ def render_cell_content(self, session, data):
+ checkin_timestamp = super(NodeInventory.CheckinColumn,
self).render_cell_content(session, data)
+ if checkin_timestamp > 0:
+ checkin_value = datetime.fromtimestamp(float(checkin_timestamp/1000000))
# the timestamps we get are too large
+ retval = fmt_datetime(checkin_value, sec=True)
+ else:
+ retval = "Never"
+ return retval
+
+ def render_header_content(self, session):
+ return "Last checkin"
+
+ class TagsColumn(ObjectTableColumn):
+ def render_cell_content(self, session, data):
+ tags_list = super(NodeInventory.TagsColumn,
self).render_cell_content(session, data)
+ tags = ""
+ if tags_list:
+ for tag in tags_list:
+ tags = tags + "," + str(tag)
+ tags = tags[1:]
+ return tags
+
+ class NodeColumn(ObjectTableColumn):
+ def render_cell_content(self, session, data):
+ node_name = super(NodeInventory.NodeColumn,
self).render_cell_content(session, data)
+ tags_list = data[1]
+ tags = ""
+ if tags_list:
+ for tag in tags_list:
+ tags = tags + "," + str(tag)
+ tags = tags[1:]
+ self.frame.tag.set_tags.form.node_name.set(session, node_name)
+ self.frame.tag.set_tags.form.tags.set(session, tags)
+ href = self.frame.tag.set_tags.get_href(session)
+ return fmt_link(href, node_name)
+
+class CreateTags(ObjectTaskForm):
+ '''
+ This form is used to display a single input field that will handle a comma separated
+ list of tags to be created.
+ '''
+ def __init__(self, app, name, task, cls):
+ super(CreateTags, self).__init__(app, name, task, cls)
+
+ self.tag = self.TagNamesField(app, "tag")
+ self.add_field(self.tag)
+
+ def process_submit(self, session):
+ tag = self.tag.get(session)
+ self.tag.validate(session)
+ if not self.errors.get(session):
+ self.tag.set(session, tag)
+
+ self.task.invoke(session, None, tag)
+
+ url = self.return_url.get(session)
+ self.page.redirect.set(session, url)
+
+ def render_tags_id(self, session):
+ return self.tag.input.path
+
+ def render_title(self, session):
+ return "Create a tag"
+
+ class TagNamesField(StringField):
+ def render_title(self, session):
+ return("Tag name(s): ")
+
+class RemoveTags(ObjectTaskForm):
+ '''
+ This form is used to allow the user to pick a set of tags to be removed.
+ '''
+ def __init__(self, app, name, task, cls):
+ super(RemoveTags, self).__init__(app, name, task, cls)
+
+ self.task = task
+
+ self.tag = self.TagsList(app, "tag")
+ self.add_field(self.tag)
+
+ def process_submit(self, session):
+ tag_to_kill = self.tag.get(session)
+
+ self.tag.validate(session)
+ if not self.errors.get(session):
+ self.task.invoke(session, None, tag_to_kill)
+
+ url = self.return_url.get(session)
+ self.page.redirect.set(session, url)
+
+ def render_tags_id(self, session):
+ return self.tag.input.path
+
+ def render_title(self, session):
+ return "Remove tags"
+
+ class TagNamesField(StringField):
+ def render_title(self, session):
+ return("Tag name(s): ")
+
+ class TagsList(CheckboxItemSetField):
+ def __init__(self, app, name):
+ item_parameter = SymbolParameter(app, "tag")
+ super(RemoveTags.TagsList, self).__init__(app, name, item_parameter)
+ self._tmpl = WidgetTemplate(self, "filtered_select_html")
+
+ def do_render(self, session):
+ writer = Writer()
+ self._tmpl.render(writer, session)
+ return writer.to_string()
+
+ def render_tags(self, session):
+ items = self.do_get_items(session)
+ tags_string = ""
+
+ for i, tag in enumerate(items):
+ tags_string = tags_string + "<option id='" + str(i) +
"' name='" + tag.title + "' value='" + tag.title +
"'" + ">" + tag.title + "</option>"
+
+ return tags_string
+
+ def do_get_items(self, session):
+ tags = fetchTags(self, session)
+ items = list()
+
+ if tags:
+ for tag in tags:
+ item = CheckboxItem(tag)
+ item.title = tag
+ items.append(item)
+
+ return items
+
+ def render_title(self, session):
+ return "Select tags to delete"
+
+
+class TagToNodes(ObjectTaskForm):
+ '''
+ This form is designed to allow many nodes to be assigned to many tags at the same
time.
+ '''
+ def __init__(self, app, name, task, cls):
+ super(TagToNodes, self).__init__(app, name, task, cls)
+
+ self.tagbox = StringParameter(app, "tagbox")
+ self.add_parameter(self.tagbox)
+
+ self.nodebox = StringParameter(app, "nodebox")
+ self.add_parameter(self.nodebox)
+
+ def process_submit(self, session):
+ nodes = self.nodebox.get(session)
+ tags = self.tagbox.get(session)
+
+ self.task.invoke(session, None, nodes, tags)
+
+ url = self.return_url.get(session)
+ self.page.redirect.set(session, url)
+
+ def render_tag_list(self, session):
+ items = fetchTags(self, session)
+ tags_string = ""
+ tags = list()
+
+ for i, tag in enumerate(items):
+ tags.append(str(tag))
+
+ return tags
+
+ def render_node_list(self, session):
+ items = fetchNodes(self, session)
+ nodes_string = ""
+ nodes = list()
+
+ for i, node in enumerate(items):
+ nodes.append(str(node))
+
+ return nodes
+
+ def render_title(self, session):
+ return "Apply tags to nodes"
+
+class TagSelectField(FormField):
+ '''
+ This specialized FormField can be used to give the user a text box
+ such that when the user enters text, they will be greeted with
+ a set of tags that match their text entry, or "No matches"
+
+ If we decide against this sort of interface, this class can be removed
+ '''
+ def __init__(self, app, name, form, param):
+ super(TagSelectField, self).__init__(app, name)
+
+ self.org_param = param
+ self.param = self.TagSearchInputSet(app, "tag_set", param)
+ self.add_child(self.param)
+
+ def get(self, session):
+ return self.org_param.get(session)
+
+ def render_title(self, session):
+ return "Tag"
+
+ def render_inputs(self, session):
+ return self.param.render(session)
+
+ def validate(self, session):
+ if self.required:
+ val = self.get(session)
+ if not val:
+ error = FormError("The %s is required" %
self.render_title(session))
+ self.form.errors.add(session, error)
+
+ class TagSearchInputSet(IncrementalSearchInput):
+ def do_get_items(self, session):
+ items = fetchTags(self, session)
+ tags_string = ""
+ tags = list()
+
+ for i, tag in enumerate(items):
+ tags.append(str(tag))
+
+ return tags
+
+ def render_item_content(self, session, tag):
+ return tag or "<em>Default</em>"
+
+ def render_item_value(self, session, tag):
+ return tag
+
+
+class EditNodeTagsForm(ObjectFrameTaskFeedbackForm):
+ def __init__(self, app, name, task):
+ super(EditNodeTagsForm, self).__init__(app, name, task)
+
+ self.node_name = self.NodeName(app, "node_name")
+ self.add_field(self.node_name)
+
+ self.tags = self.Tags(app, "tags")
+ self.add_field(self.tags)
+
+ self.new_tags = self.TagsList(app, "ptag")
+ self.add_field(self.new_tags)
+
+ self.update_enabled = False
+
+
+ def process_submit(self, session):
+ orig_tags_value = self.tags.input.get(session)
+ new_tags_value = self.new_tags.get(session)
+
+ self.tags.validate(session)
+ if not self.errors.get(session):
+ self.tags.set(session, new_tags_value)
+
+ node_name = self.node_name.get(session)
+ negotiator = self.object.get(session)
+
+ self.task.invoke(session, negotiator, node_name, new_tags_value)
+ self.task.exit_with_redirect(session)
+
+ def render_form_class(self, session):
+ return " ".join((super(EditNodeTagsForm,
self).render_form_class(session), "mform"))
+
+ class TagsList(CheckboxItemSetField):
+ '''
+ This class, note the rendering handled in
tags.strings::double_filtered_select_html,
+ takes care of displaying the tags for the given node in a filterable select box
with additions
+ being moved to a second box.
+ '''
+ def __init__(self, app, name):
+ item_parameter = SymbolParameter(app, "tag")
+ super(EditNodeTagsForm.TagsList, self).__init__(app, name, item_parameter)
+ self._tmpl = WidgetTemplate(self, "double_filtered_select_html")
+
+ self.update_enabled = False
+
+ def do_render(self, session):
+ writer = Writer()
+ self._tmpl.render(writer, session)
+ return writer.to_string()
+
+ def render_tags(self, session):
+ items = self.do_get_items(session)
+ tags_string = ""
+
+ for i, tag in enumerate(items):
+ tags_string = tags_string + "<option id='" + str(i) +
"'>" + tag.title + "</option>\n"
+
+ return tags_string
+
+ def render_starting_tags(self, session):
+ tags_from_session= self.form.tags.get(session)
+ tags_string = ""
+ items = []
+ if tags_from_session != "":
+ items = [x.strip() for x in tags_from_session.split(',')]
+
+ for i, tag in enumerate(items):
+ tags_string = tags_string + "<option id='init" + str(i)
+ "'>" + tag + "</option>\n"
+
+ return tags_string
+
+ def do_get_items(self, session):
+ tags = fetchTags(self, session)
+
+ items = list()
+
+ if tags:
+ for tag in tags:
+ item = CheckboxItem(tag)
+ item.title = tag
+ items.append(item)
+
+ return items
+
+ def render_title(self, session):
+ return "Update tags:"
+
+ class NodeName(StringField):
+ def __init__(self, app, name):
+ super(EditNodeTagsForm.NodeName, self).__init__(app, name)
+
+ self.input = self.DisabledInput(app, "input")
+ self.replace_child(self.input)
+
+ def get(self,session):
+ value = self.input.get(session)
+ return value
+
+ def render_title(self, session):
+ return "Node name"
+
+ class DisabledInput(StringInput):
+ pass
+
+ class Tags(StringField):
+ def __init__(self, app, name):
+ super(EditNodeTagsForm.Tags, self).__init__(app, name)
+
+ self.input = self.DisabledInput(app, "input")
+ self.replace_child(self.input)
+
+ def render_title(self, session):
+ return "Current tags"
+
+ def get(self,session):
+ value = self.input.get(session)
+ return value
+
+ class DisabledInput(StringInput):
+ pass
+
+class EditTagNodesForm(ObjectFrameTaskFeedbackForm):
+ def __init__(self, app, name, task):
+ super(EditTagNodesForm, self).__init__(app, name, task)
+
+ self.tags = self.Tags(app, "tags")
+ self.add_field(self.tags)
+
+ self.node_name = self.NodeName(app, "node_name")
+ self.add_field(self.node_name)
+
+ self.possible_nodes = self.NodesList(app, "pnod")
+ self.add_field(self.possible_nodes)
+
+ self.update_enabled = False
+
+
+ def process_submit(self, session):
+ tag = self.tags.input.get(session)
+ nodes_value = self.possible_nodes.get(session)
+
+ self.tags.validate(session)
+ if not self.errors.get(session):
+ self.tags.set(session, tag)
+
+ self.task.invoke(session, None, tag, nodes_value)
+ self.task.exit_with_redirect(session)
+
+ def render_form_class(self, session):
+ return " ".join((super(EditTagNodesForm,
self).render_form_class(session), "mform"))
+
+ class NodesList(CheckboxItemSetField):
+ def __init__(self, app, name):
+ item_parameter = SymbolParameter(app, "tag")
+ super(EditTagNodesForm.NodesList, self).__init__(app, name, item_parameter)
+ self._tmpl = WidgetTemplate(self, "filtered_select_html")
+
+ def do_render(self, session):
+ writer = Writer()
+ self._tmpl.render(writer, session)
+ return writer.to_string()
+
+ def render_nodes(self, session):
+ items = self.do_get_items(session)
+ nodes_string = ""
+
+ given_tag = self.form.tags.input.param.get(session)
+ selected_nodes = self.app.wallaby.get_node_names(given_tag)
+
+ for i, node in enumerate(items):
+ selected = ""
+ if(selected_nodes and node.title in selected_nodes):
+ selected = " selected='selected' "
+ nodes_string = nodes_string + "<option id='" + str(i) +
"' name='" + node.title + "' value='" + node.title +
"'" + selected + ">" + node.title +
"</option>"
+
+ return nodes_string
+
+ def do_get_items(self, session):
+ nodes = fetchNodes(self, session)
+ items = list()
+
+ if nodes:
+ for node in nodes:
+ item = CheckboxItem(node)
+ item.title = node
+ items.append(item)
+
+ return items
+
+ def render_title(self, session):
+ return "Tags"
+
+ class NodeName(StringField):
+ def __init__(self, app, name):
+ super(EditTagNodesForm.NodeName, self).__init__(app, name)
+
+ self.input = self.DisabledInput(app, "input")
+ self.replace_child(self.input)
+
+ def get(self,session):
+ value = self.input.get(session)
+ if not value or value == "":
+ value = "No nodes currently selected"
+ return value
+
+ def render_title(self, session):
+ return "Current nodes:"
+
+ class DisabledInput(StringInput):
+ pass
+
+ class Tags(StringField):
+ def __init__(self, app, name):
+ super(EditTagNodesForm.Tags, self).__init__(app, name)
+
+ self.input = self.DisabledInput(app, "input")
+ self.replace_child(self.input)
+
+ def render_title(self, session):
+ return "Tag:"
+
+ def get(self,session):
+ value = self.input.get(session)
+ return value
+
+ class DisabledInput(StringInput):
+ pass
+
+class TagsNodeEditTask(ObjectFrameTask):
+ '''
+ On invocation, this task will take a node and reset the tags for that node to a
+ set of tags that are specified on the invocation
+ '''
+ def __init__(self, app, frame):
+ super(TagsNodeEditTask, self).__init__(app, frame)
+
+ self.form = EditNodeTagsForm(app, self.name, self)
+ self.visible = False
+
+ def get_title(self, session):
+ return "Change tags associated with this node"
+
+ def do_enter(self, session, osession):
+ self.form.tags.set(session, self.form.tags.get(osession))
+ self.form.node_name.set(session, self.form.node_name.get(osession))
+
+ def do_invoke(self, invoc, negotiator, node_name, tags):
+ try:
+ call_async(invoc.make_callback(), self.app.wallaby.edit_tags, node_name,
*tags)
+ except Exception, e:
+ invoc.status = invoc.FAILED
+ log.exception(e)
+
+ invoc.end()
+
+class TagsTagEditTask(ObjectFrameTask):
+ '''
+ This is the task that will take a tag and a set of nodes and make the
+ necessary assignments or unassignments.
+ '''
+ def __init__(self, app, frame):
+ super(TagsTagEditTask, self).__init__(app, frame)
+
+ self.form = EditTagNodesForm(app, self.name, self)
+
+ def get_title(self, session):
+ return "Change nodes for this tag "
+
+ def do_enter(self, session, osession):
+ self.form.tags.set(session, self.form.tags.get(osession))
+ self.form.node_name.set(session, self.form.node_name.get(osession))
+
+ def do_invoke(self, invoc, negotiator, tag, chosen_nodes):
+ '''
+ if the node is on the current_nodes list and is on the chosen_nodes passed in,
nothing to do
+ ** if the node is NOT on the current_nodes list and is on the chosen_nodes passed
in, add this tag to that node
+ ** if the node is on the current_nodes list and is NOT on the chosen_nodes passed
in, update that node sans this tag
+ if the node is NOT on the current_nodes list and is NOT on the chosen_nodes
passed in, nothing to do
+ '''
+ current_nodes = self.app.wallaby.get_node_names(tag)
+ try:
+ for node in chosen_nodes:
+ if node not in current_nodes:
+ #we need to add the new tag to the existing tags for each node in the
list
+ current_tags = self.app.wallaby.get_tag_names(node)
+ current_tags.append(tag)
+ call_async(invoc.make_callback(), self.app.wallaby.edit_tags, node,
*current_tags)
+ for node in current_nodes:
+ if node not in chosen_nodes:
+ current_tags = self.app.wallaby.get_tag_names(node)
+ current_tags.remove(tag)
+ call_async(invoc.make_callback(), self.app.wallaby.edit_tags, node,
*current_tags)
+ except Exception, e:
+ invoc.status = invoc.FAILED
+ log.exception(e)
+
+ invoc.end()
+
+class AddTags(Task):
+ '''
+ This task is used to create a tag in wallaby without assigning it to any nodes.
+ '''
+ def __init__(self, app):
+ super(AddTags, self).__init__(app)
+ cls = app.model.com_redhat_cumin_grid.Node
+ self.form = CreateTags(app, self.name, self, cls)
+
+ def get_title(self, session, object):
+ return "Create tag(s)"
+
+ def do_invoke(self, session, object, invoc, tag):
+ tags = [x.strip() for x in tag.split(',')]
+
+ try:
+ for new_tag in tags:
+ call_async(invoc.make_callback(), self.app.wallaby.create_tag, new_tag)
+ except Exception, e:
+ invoc.status = invoc.FAILED
+ log.exception(e)
+
+ invoc.end()
+
+class ChangeNodeTags(Task):
+ '''
+ This is the task that is invoked to change the tags for a given set of
+ nodes and tags. It will go through each node in the list and set the
+ tags list of that node to the supplied tags, supporting whatever "many to
many"
+ UI elements are in front of this.
+ '''
+ def __init__(self, app):
+ super(ChangeNodeTags, self).__init__(app)
+ cls = app.model.com_redhat_cumin_grid.Node
+ self.form = TagToNodes(app, self.name, self, cls)
+
+ def get_title(self, session, object):
+ return "Edit node tags"
+
+ def do_invoke(self, session, object, invoc, nodes, tags):
+ tag_list = [x.strip() for x in tags.split(',')]
+ node_list = [x.strip() for x in nodes.split(',')]
+
+ try:
+ for node in node_list:
+ updated_tags = self.app.wallaby.get_tag_names(node)
+ for tag in tag_list:
+ if tag not in updated_tags:
+ updated_tags.append(tag)
+ call_async(invoc.make_callback(), self.app.wallaby.edit_tags, node,
*updated_tags)
+
+ #self.app.wallaby.edit_tags(node, *updated_tags)
+
+ except Exception, e:
+ invoc.status = invoc.FAILED
+ log.exception(e)
+
+ invoc.end()
+
+class RemoveNodeTags(Task):
+ '''
+ This is the task that is invoked to trigger the deletion of a given tag.
+ It will call WallabyOperations.remove_tag, which in turn will remove the tag from
+ all nodes it is currently associated with and then will remove the tag from wallaby
entirely.
+ '''
+ def __init__(self, app):
+ super(RemoveNodeTags, self).__init__(app)
+ cls = app.model.com_redhat_cumin_grid.Node
+ self.form = RemoveTags(app, self.name, self, cls)
+
+ def get_title(self, session, object):
+ return "Delete tag(s)"
+
+ def do_invoke(self, session, object, invoc, tags):
+ try:
+ for tag_to_kill in tags:
+ call_async(invoc.make_callback(), self.app.wallaby.remove_tag,
tag_to_kill)
+ except Exception, e:
+ invoc.status = invoc.FAILED
+ log.exception(e)
+
+ invoc.end()
+
+
+class TagsEditor(RadioModeSet):
+ '''
+ This defines the set of radio buttons that appear under the Tags tab in the UI
+ '''
+ def __init__(self, app, name):
+ super(TagsEditor, self).__init__(app, name)
+
+ inventory_tab = NodeInventory(app, "nodi")
+ self.add_tab(inventory_tab)
+
+ tag_inventory_tab = TagInventory(app, "tagi")
+ self.add_tab(tag_inventory_tab)
+
+ def render_title(self, session):
+ return "Tags"
+
+
+def fetchTags(self, session):
+ '''
+ fetch the list of tags from wallaby
+ '''
+ wallaby_tags = self.app.wallaby.get_data(WBTypes.TAGS)
+ tag_list = list()
+
+ for i, tag in enumerate(wallaby_tags):
+ tag_list.append(xml_escape(str(tag.name)))
+ tag_list.sort()
+
+ return tag_list
+
+def fetchNodes(self, session):
+ '''
+ fetch the list of nodes from wallaby
+ '''
+ wallaby_nodes = self.app.wallaby.get_data(WBTypes.NODES)
+ node_list = list()
+
+ for i, node in enumerate(wallaby_nodes):
+ node_list.append(xml_escape(node.name))
+ node_list.sort()
+
+ return node_list
\ No newline at end of file
Added: trunk/cumin/python/cumin/grid/tags.strings
===================================================================
--- trunk/cumin/python/cumin/grid/tags.strings (rev 0)
+++ trunk/cumin/python/cumin/grid/tags.strings 2011-08-05 18:58:51 UTC (rev 4907)
@@ -0,0 +1,229 @@
+[DisabledInput.css]
+input.disabled {
+ border: 0px;
+ background-color: white;
+ color: black;
+ cursor:default;
+}
+
+[DisabledInput.html]
+{value}
+<input type="hidden" name="{name}" id="{name}"
value="{value}" tabindex="{tab_index}" size="{size}"/>
+
+[TagToNodes.html]
+<form id="{id}" class="ButtonForm" method="post"
action="?">
+ <div class="title">{title}</div>
+ <div class="content">
+ <div class='lfloat'>
+ <div>
+ Nodes
+ </div>
+ <div>
+ <textarea id='{id}.nodebox' name='{id}.nodebox' cols='30'
rows='5' size='30' onfocus="javascript:if(this.value=='')
{if(node_completer)node_completer.observer.onFired();}" />
+ </div>
+ </div>
+ <div class='rfloat'>
+ <div>
+ Tags
+ </div>
+ <div>
+ <textarea id='{id}.tagbox' name='{id}.tagbox' cols='30'
rows='5' size='30' onfocus="javascript:if(this.value=='')
{if(tag_completer)tag_completer.observer.onFired();}" />
+ </div>
+ </div>
+ <div style="clear:both;"> </div>
+ </div>
+ <div class="buttons">
+ {submit}
+ {cancel}
+ </div>
+ <div>{hidden_inputs}</div>
+</form>
+<script type="text/javascript">
+//<![CDATA[
+
+ updateResults = function() {
+ $('{id}.results').innerHTML = "";
+ for(var i=0; i < $('{id}.tagbox').options.length; i++) {
+ if ($('{id}.tagbox').options[i].selected == true) {
+ $('{id}.results').innerHTML = $('{id}.results').innerHTML +
$('{id}.tagbox').options[i].text + ": ";
+ for(var j=0; j < $('{id}.nodebox').options.length; j++) {
+ if ($('{id}.nodebox').options[j].selected == true) {
+ $('{id}.results').innerHTML = $('{id}.results').innerHTML +
$('{id}.nodebox').options[j].text + " ";
+ }
+ }
+ $('{id}.results').innerHTML = $('{id}.results').innerHTML +
"<br/>";
+ }
+ }
+ }
+
+var node_completer = null;
+var tag_completer = null;
+document.addEvent('domready', function() {
+ var tokens = {node_list};
+ var tags = {tag_list};
+
+ node_completer = new Autocompleter.Local('{id}.nodebox', tokens, {
+ 'minLength': 1, // We need at least 1 character
+ 'selectMode': 'type-ahead', // Instant completion
+ 'multiple': true, // Tag support, by default comma separated
+ 'overflow': true,
+ 'filterSubset': true,
+ 'zindex': 50000
+ });
+
+ tag_completer = new Autocompleter.Local('{id}.tagbox', tags, {
+ 'minLength': 1, // We need at least 1 character
+ 'selectMode': 'type-ahead', // Instant completion
+ 'multiple': true, // Tag support, by default comma separated
+ 'overflow': true,
+ 'filterSubset': true,
+ 'zindex': 50000
+ });
+});
+
+
+//]]>
+</script>
+
+
+[NodesList.filtered_select_html]
+ <script type="text/javascript">
+//<![CDATA[
+window.addEvent('domready', function (){
+ $$('.multiselect').each(function(multiselect){
+ new MTMultiWidget({'datasrc': multiselect,
+ 'widgetcls':'mtmultiselect',
+ 'selectedcls':'selected',
+ 'paginator_on_bottom':'true',
+ 'items_per_page':15,
+ 'case_sensitive':false,
+ 'setview':'total'});
+ });
+});
+
+//]]>
+ </script>
+ <tr>
+ <th><div class="title">Nodes:</div></th>
+ <td>
+ <div style='width:250px;height:375px;'>
+ <select class='multiselect' name="{id}.inputs.param"
multiple="multiple" id="{id}.inputs.param" size="10"
style="width:400px">
+ {nodes}
+ </select>
+ </div>
+ </td>
+ </tr>
+
+
+[TagsList.filtered_select_html]
+ <script type="text/javascript">
+//<![CDATA[
+window.addEvent('domready', function (){
+ $$('.multiselect').each(function(multiselect){
+ new MTMultiWidget({'datasrc': multiselect,
+ 'widgetcls':'mtmultiselect',
+ 'selectedcls':'selected',
+ 'paginator_on_bottom':'true',
+ 'items_per_page':15,
+ 'case_sensitive':false,
+ 'setview':'total'});
+ });
+});
+
+//]]>
+ </script>
+ <tr>
+ <th><div class="title">{title}</div></th>
+ <td>
+ <div style='width:250px;height:375px;'>
+ <select class='multiselect' name="{id}.inputs.param"
multiple="multiple" id="{id}.inputs.param" size="10"
style="width:400px">
+ {tags}
+ </select>
+ </div>
+ </td>
+ </tr>
+
+
+
+
+
+
+[TagsList.double_filtered_select_html]
+ <script type="text/javascript">
+//<![CDATA[
+ MyUtil = new Object();
+ MyUtil.selectFilterData = new Object();
+ MyUtil.selectFilter = function(selectId, filter) {
+ var list = document.getElementById(selectId);
+ if(!MyUtil.selectFilterData[selectId]) { //if we don't have a list of all the
options, cache them now'
+ MyUtil.selectFilterData[selectId] = new Object();
+ for(var i = 0; i < list.options.length; i++)
MyUtil.selectFilterData[selectId][list.options[i].id] = list.options[i];
+ }
+
+ //go through all the options in the current list and set the selection status to the
cached options
+ for(var i = 0; i < list.options.length; i++)
MyUtil.selectFilterData[selectId][list.options[i].id] = list.options[i];
+
+ list.options.length = 0; //remove all elements from the list
+ for(var id in MyUtil.selectFilterData[selectId]) { //add elements from cache if they
match filter
+ var o = MyUtil.selectFilterData[selectId][id];
+ if(o.text.toLowerCase().indexOf(filter.toLowerCase()) >= 0) list.add(o, null);
+ }
+ }
+
+ window.addEvent('domready', function() {
+ $('add').addEvent('click', function() {
+ $('{id}2.inputs.param').getSelected().each(function(el) {
+ el.inject($('{id}.inputs.param'));
+ });
+ });
+ $('remove').addEvent('click', function() {
+ $('{id}.inputs.param').getSelected().each(function(el) {
+ el.inject($('{id}2.inputs.param'));
+ });
+ });
+ });
+
+ // when the form is submitted, we want to grab everything in the "selected
tags" box whether it is highlighted or not
+ $$('form').each(function(form) {
+ form.addEvent('submit',function(event){
+ sbox = $('{id}.inputs.param');
+ for(var i = 0; i < sbox.length; i++) {
+ sbox.options[i].selected = true;
+ }
+ });
+ });
+//]]>
+ </script>
+
+ <tr>
+ <th><div class="title">Filter available
tags</div></th>
+ <td>
+ <input id='{id}.filter' type="text"
onkeyup="MyUtil.selectFilter('{id}2.inputs.param',
$('{id}.filter').value);"/>
+ </td>
+ </tr>
+ <tr>
+ <th><div class="title">Tags</div></th>
+ <td>
+ <div style="width:500px;">
+ <div class='lfloat'>
+ <div><label for='{id}2.inputs.param'
class='title'>Available Tags</label></div>
+ <div>
+ <select name="{id}2.inputs.param" multiple="multiple"
id="{id}2.inputs.param" size="10" style="width:200px">
+ {tags}
+ </select>
+ </div>
+ <div><input type="button" id="add"
onclick="javascript:return false;" value="Add to selected
tags"/></div>
+ </div>
+ <div class='rfloat'>
+ <div><label for='{id}.inputs.param'
class='title'>Selected Tags</label></div>
+ <div>
+ <select name="{id}.inputs.param" multiple="multiple"
id="{id}.inputs.param" size="10" style="width:200px">
+ {starting_tags}
+ </select>
+ </div>
+ <div><input type="button" id="remove"
onclick="javascript:return false;" value="Remove"/></div>
+ </div>
+ </div>
+ </td>
+ </tr>
+
Modified: trunk/cumin/resources/app.css
===================================================================
--- trunk/cumin/resources/app.css 2011-08-04 16:40:02 UTC (rev 4906)
+++ trunk/cumin/resources/app.css 2011-08-05 18:58:51 UTC (rev 4907)
@@ -403,6 +403,10 @@
float: right;
}
+.lfloat {
+ float: left;
+}
+
.rclear {
font-size:0.01em;
width: 0.01px;
@@ -659,3 +663,109 @@
div.OverviewView {
clear: both;
}
+
+ .mtmultiselect{
+ width: 400px;
+ background: #0D171A;
+ font: 12px verdana;
+ }
+ .mtmultiselect .selected{
+ background-color: #E8F7E8;
+ }
+ .mtmultiselect ol .selected{
+ background-color: #E8F7E8;
+ background-image: url('resource?name=check-mark.png');
+ background-position: left;
+ background-repeat: no-repeat;
+ }
+ .mtmultiselect ul li{
+ display:inline;
+ }
+ .disabled{
+ color: #ccc;
+ }
+ .mtms_filterbox{
+ float: left;
+ padding: 5px;
+ }
+ .mtms_filterbox input{
+ background: #FFF;
+ border: 1px solid #141212;
+ }
+ .mtms_filtercontrols{
+ padding: 7px 0;
+ color: #B4A6A6;l
+ }
+ .mtms_filtercontrols a, .mtms_paginator a,
+ .mtms_filterlabel{
+ color: #8C8CD9;
+ padding: 0 4px 0 6px;
+ }
+ .mtms_paginator{
+ clear: both;
+ padding: 5px;
+ }
+ .mtmultiselect ol{
+ padding: 0;
+ }
+ .mtmultiselect ol li{
+ list-style-type: none;
+ padding: 1px 5px 1px 25px;
+ background-color: #FFF;
+ color: #000;
+ }
+ .mtmultiselect ol li:hover{
+ background-color: #84D1E0;
+ color: #111;
+ }
+
+ul.autocompleter-choices
+{
+ position: absolute;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ border: 1px solid #7c7c7c;
+ border-left-color: #c3c3c3;
+ border-right-color: #c3c3c3;
+ border-bottom-color: #ddd;
+ background-color: #fff;
+ text-align: left;
+ font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
+ z-index: 50;
+ background-color: #fff;
+}
+
+ul.autocompleter-choices li
+{
+ position: relative;
+ margin: -2px 0 0 0;
+ padding: 0.2em 1.5em 0.2em 1em;
+ display: block;
+ float: none !important;
+ cursor: pointer;
+ font-weight: normal;
+ white-space: nowrap;
+ font-size: 1em;
+ line-height: 1.5em;
+}
+
+ul.autocompleter-choices li.autocompleter-selected
+{
+ background-color: #444;
+ color: #fff;
+}
+
+ul.autocompleter-choices span.autocompleter-queried
+{
+ display: inline;
+ float: none;
+ font-weight: bold;
+ margin: 0;
+ padding: 0;
+}
+
+ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried
+{
+ color: #9FCFFF;
+}
\ No newline at end of file
Modified: trunk/wooly/python/wooly/forms.strings
===================================================================
--- trunk/wooly/python/wooly/forms.strings 2011-08-04 16:40:02 UTC (rev 4906)
+++ trunk/wooly/python/wooly/forms.strings 2011-08-05 18:58:51 UTC (rev 4907)
@@ -313,6 +313,9 @@
</form>
<script type="text/javascript">
//<![CDATA[
- $("{id}").elements[0].focus();
+ var focusElement = $("{id}").elements[0];
+ if( focusElement != "hidden" && focusElement.style.display !=
"none" && !focusElement.disabled && focusElement.type !=
"hidden" ) {
+ focusElement.focus();
+ }
//]]>
</script>
Modified: trunk/wooly/resources/mootools.js
===================================================================
--- trunk/wooly/resources/mootools.js 2011-08-04 16:40:02 UTC (rev 4906)
+++ trunk/wooly/resources/mootools.js 2011-08-05 18:58:51 UTC (rev 4907)
@@ -1482,4 +1482,535 @@
String.implement({findAllEmails:function(){return this.match(new
RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?","gi"))||[];}});String.alias('parseQueryString','parseQuery');
var Waiter=new
Class({Extends:Spinner,options:{baseHref:'http://www.cnet.com/html/rb...
Element('img',$merge(this.options.img,{src:this.options.baseHref+this.options.img.src})).inject(this.img);this.element.set(this.options.layer);},place:function(){this.inject.apply(this,arguments);},reset:function(){return
this.hide();},start:function(element){return this.show();},stop:function(callback){return
this.hide();}});if(window.Request){Request=Class.refactor(Request,{options:{useWaiter:false,waiterOptions:{},waiterTarget:false},initialize:function(options){if(options){if(options.useWaiter)options.useSpinner=options.useWaiter;if(options.waiterOptions)options.spinnerOptions=options.waiterOptions;if(options.waiterTarget)options.spinnerTarget=options.waiterTarget;}
this.previous(options);}});}
-Element.Properties.waiter={set:function(options){return
this.set('spinner',options);},get:function(options){return
this.get('spinner',options);}};Element.implement({wait:function(options){return
this.spin(options);},release:function(){return this.unspin(options);}});
\ No newline at end of file
+Element.Properties.waiter={set:function(options){return
this.set('spinner',options);},get:function(options){return
this.get('spinner',options);}};Element.implement({wait:function(options){return
this.spin(options);},release:function(){return this.unspin(options);}});
+
+/*
+Copyright (c) 2009 Justin Donato (
http://www.justindonato.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+MTMultiSelect: An widget to make more user friendly interfaces
+for a multiselect box, using Mootools
+
+Usage:
+
+Link to this file, then add
+
+window.addEvent('domready', function (){
+ $$('.multiselect').each(function(multiselect){
+ new MTMultiWidget({'datasrc': multiselect});
+ });
+
+});
+
+to your page, where '.multiselect' is a selector that picks out a class of
+<select MULTIPLE> elements. This element acts as the data source for the
+widget. Then create a css file and style the widget however you'd like.
+
+Options: add these to the object you pass to the MTMultiWidget constructor.
+Only datasrc is required.
+
+widgetcls:
+The css class applied to your final widget.
+Default is 'mtmultiselect'
+
+datasrc:
+A select multiple dom object
+
+selectedcls:
+The css class of the selected list item.
+Default is 'selected'
+
+paginator_on_bottom:
+Determines placement of paginator, true or false.
+Default is true
+
+items_per_page:
+Determines how many items to show on a single page.
+Default is 10.
+
+case_sensitive:
+Determines if the filter form is case_sensitive or not
+Default is False
+
+setview:
+Determines what view to show on initialization
+Possible values are 'selected', 'unselected' or 'total'
+default is 'total'
+*/
+
+var MTMultiWidget = new Class({
+
+ Implements: [Options],
+
+ options: {
+ 'widgetcls': 'mtmultiselect', // the class for your final widget
+ 'datasrc': null // a select multiple dom object
+ },
+
+ handleDisplayEvent: function(numselected){
+ this.filterform.update(numselected);
+ },
+
+ handleFilterEvent: function(list){
+ this.curlist = list;
+ this.paginator.setpagenum(1);
+ var page = this.paginator.getpage(list);
+ this.displaylist.build(page);
+ },
+
+ handlePaginatorEvent: function(){
+ var page = this.paginator.getpage(this.curlist);
+ this.displaylist.build(page);
+ },
+
+ initialize: function(options){
+ // Hide the original data source and create a new div for the widget
+ // and inject this new widget into the Dom.
+ options.datasrc.setStyles({'display': 'None'});
+ var view = new Element('div', {'class':
this.options.widgetcls});
+ view.injectAfter(options.datasrc);
+
+ // Add the newly created widget to the options obj so other
+ // components have a reference to it.
+ options.view = view;
+ this.setOptions(options);
+
+ this.displaylist = new DisplayList(options);
+ options.numselected = this.displaylist.numselected();
+
+ this.filterform = new FilterForm(options);
+ this.paginator = new Paginator(options);
+
+ this.curlist = this.initialdata(options)
+
+ this.displaylist.addEvent('rebuild',
this.handleDisplayEvent.bind(this));
+ this.filterform.addEvent('rebuild', this.handleFilterEvent.bind(this));
+ this.paginator.addEvent('rebuild',
this.handlePaginatorEvent.bind(this));
+
+ var page = this.paginator.getpage(this.curlist);
+ this.filterform.build();
+ this.displaylist.build(page);
+ this.setview(this.filterform, this.displaylist, options)
+ },
+
+ setview: function(filterform, displaylist, options){
+ if(options.setview == 'selected'){
+ this.filterform.showselected();
+ }
+ else if(options.setview == 'unselected'){
+ this.filterform.showunselected();
+ }
+
+ },
+
+ initialdata: function(options){
+ // moved this out to a method in case you want to implement an
+ // ajax datasrc.
+ return this.options.datasrc.getChildren();
+ },
+
+ refresh: function(){
+ this.setview(this.filterform, this.displaylist, this.options);
+ this.filterform.update(this.displaylist.numselected());
+ return this;
+ }
+});
+
+/*
+ DisplayList
+*/
+
+var DisplayList = new Class({
+
+ Implements: [Options, Events],
+
+ options: {
+ selectedcls: 'selected', // class of the item when its selected
+ datasrc: null, // a multiple select dom element
+ view: null, // A parent or wrapper dom element where this element lives
+ paginator_on_bottom: true, // determines location of paginator
+ represent: null
+ },
+
+ initialize: function(options){
+ this.setOptions(options);
+ },
+
+ build: function(opts){
+
+ // If there's already an ol, remove it.
+ var old = this.options.view.getElement('ol');
+ if(old !== null) old.destroy();
+
+ // create the list to hold the visible elements
+ list = new Element('ol');
+ place = this.options.paginator_on_bottom ? 'before' : 'after';
+ list.inject(this.options.view.getLast(), place);
+ //this.options.view.grab(list);
+
+ opts.each(function(item){
+ var li = new Element('li',{
+ 'class': item.selected ? this.options.selectedcls : null
+ });
+
+ var repr;
+ if(this.options.represent){
+ repr = this.options.represent(item, li);
+ }
+ else{
+ repr = item.get('text');
+ }
+
+ li.set('text', repr);
+
+ li.store('select', item);
+ list.grab(li);
+
+ li.addEvent('click', function(evt){
+ evt.target.toggleClass('selected');
+ evt.target.retrieve('select').selected =
evt.target.hasClass('selected');
+ this.fireEvent('rebuild', this.numselected());
+ }.bind(this));
+ }.bind(this));
+ },
+
+ total: function(){
+ return this.options.datasrc.getChildren().length;
+ },
+
+ numselected: function(){
+ var numselected = 0;
+ this.options.datasrc.getChildren().each(function(item){
+ if(item.selected) numselected++;
+ });
+ return numselected;
+ }
+
+});
+
+/*
+ Paginator
+*/
+
+var Paginator = new Class({
+ Implements: [Options, Events],
+
+ options: {
+ 'items_per_page': 10,
+ 'list': [], // the list the paginator will use
+ 'displaylist': null // the view element for the results
+ },
+
+ initialize: function(options){
+ this.setOptions(options);
+ this.items_per_page = this.options.items_per_page;
+ this.page = 1;
+ // create the view element
+ this.controls = new Element('div',
{'class':'mtms_paginator'});
+ this.options.view.grab(this.controls);
+ },
+
+ numpages: function(list){
+ return Math.ceil(list.length / this.items_per_page );
+ },
+
+ getpage: function(list){
+ var start = ((this.page - 1) * this.items_per_page);
+ var end = start + this.items_per_page;
+ this.updateControls(list);
+ return list.slice(start, end);
+ },
+
+ pageup: function(){
+ this.page++;
+ },
+
+ pagedown: function(){
+ this.page--;
+ },
+
+ pagefirst: function() {
+ this.page=1;
+ },
+
+ pagelast: function(lastpagenum) {
+ this.page=lastpagenum;
+ },
+
+ updateControls: function(list){
+
+ var numpages = this.numpages(list);
+ this.controls.empty();
+
+ var firstbtn = new Element('a', {'text':'first',
+ 'href':'javascript:void(0)'});
+ this.controls.grab(firstbtn, 'top');
+ firstbtn.addClass('disabled');
+
+ if(this.page > 1){
+ firstbtn.removeClass('disabled');
+ firstbtn.addEvent('click', function(evt){
+ this.pagefirst();
+ this.fireEvent('rebuild', list);
+ }.bind(this));
+ }
+
+ var prevbtn = new Element('a', {'text':'prev',
+ 'href':'javascript:void(0)'});
+ this.controls.grab(prevbtn, 'bottom');
+ prevbtn.addClass('disabled');
+
+ if(this.page > 1){
+ prevbtn.removeClass('disabled');
+ prevbtn.addEvent('click', function(evt){
+ this.pagedown();
+ this.fireEvent('rebuild', list);
+ }.bind(this));
+ }
+
+ for(var i = this.page; i <= this.page + 6 && i <= numpages; i++){
+ this.controls.grab(new Element('a', {'text': i,
+ 'href': '#cpcspswdnbd',
+ 'class': (i == this.page) ?
'selected' : '',
+ 'events': {
+ 'click': function(evt){
+
this.setpagenum(evt.target.innerHTML);
+ this.fireEvent('rebuild',
[list]); // wrapped list
+ }.bind(this)}
+ }), 'bottom');
+ }
+
+ var nextbtn = new Element('a', {'text':'next',
+ 'href':'#cpcspswdnbd'});
+ this.controls.grab(nextbtn);
+ nextbtn.addClass('disabled');
+
+ if(this.page < numpages){
+ nextbtn.removeClass('disabled');
+ nextbtn.addEvent('click', function(evt){
+ this.pageup();
+ this.fireEvent('rebuild');
+ }.bind(this));
+ }
+
+ var lastbtn = new Element('a', {'text':'last',
+ 'href':'#cpcspswdnbd'});
+ this.controls.grab(lastbtn);
+ lastbtn.addClass('disabled');
+
+ if(this.page < numpages){
+ lastbtn.removeClass('disabled');
+ lastbtn.addEvent('click', function(evt){
+ this.pagelast(numpages);
+ this.fireEvent('rebuild');
+ }.bind(this));
+ }
+
+
+ },
+
+ setpagenum: function(pagenum){
+ this.page = Number(pagenum);
+ }
+});
+
+/*
+ FilterForm
+*/
+
+var FilterForm = new Class({
+
+ RESETINPUT: true,
+
+ Implements: [Options, Events],
+
+ options: {
+ view: null,
+ case_sensitive: false,
+ displaylist: null,
+ inputpos: 'top',
+ labels: {'total': 'total',
+ 'selected': 'selected',
+ 'unselected': 'unselected',
+ 'selectall': 'select all',
+ 'selectnone': 'select none',
+ 'filter': 'filter',
+ 'in': 'in',
+ 'out': 'out',
+ 'page': 'page'
+ },
+ classes: {
+ 'total': 'mttotal',
+ 'selected': 'mtselected'
+ }
+ },
+
+ initialize: function(options){
+ this.setOptions(options);
+ this.numselected = options.numselected;
+ },
+
+ build: function(){
+ // infofilter bar is made out of a u list
+ var ul = new Element('ul', {'class':
'mtms_filtercontrols'});
+ this.options.view.grab(ul, this.options.inputpos);
+
+ this.totalbtn = this.makebtn(this.options.labels.total,
+ this.showtotal,
+ this.options.datasrc.getChildren().length);
+ ul.grab(this.totalbtn);
+
+ this.selectedbtn = this.makebtn(this.options.labels.selected,
+ this.showselected,
+ this.numselected);
+ ul.grab(this.selectedbtn);
+
+ this.unselectedbtn = this.makebtn(this.options.labels.unselected,
+ this.showunselected,
+ this.options.datasrc.getChildren().length -
this.numselected);
+ ul.grab(this.unselectedbtn);
+
+ this.selectAllBtn = this.makebtn(this.options.labels.selectall,
+ this.selectAll,
+ '');
+ ul.grab(this.selectAllBtn);
+
+ this.selectNoneBtn = this.makebtn(this.options.labels.selectnone,
+ this.selectNone,
+ '');
+ ul.grab(this.selectNoneBtn);
+
+ // Make text field for filter
+ // On keyup, the displaylist.filter is called with a function
+ // that filters based on what's been entered in the textfield
+ filterbox_container = new Element('div', {'class':
'mtms_filterbox'});
+ this.filterbox = new Element('input', {
+ 'events': {
+ 'keyup': function(evt){
+ if(this.options.case_sensitive){
+ filter_by_text = function(item){ return
item.text.contains(evt.target.value)};
+ } else {
+ filter_by_text = function(item){ return
item.text.toLowerCase().contains(evt.target.value.toLowerCase()) };
+ }
+ this.filter(this.options.datasrc.getChildren(), filter_by_text);
+ }.bind(this)
+ }
+ });
+ this.filterbox_label = new Element('span', {'html':'Filter
list: ', 'class':'mtms_filterlabel'});
+ filterbox_container.grab(this.filterbox_label);
+ filterbox_container.grab(this.filterbox);
+ this.options.view.grab(filterbox_container, this.options.inputpos);
+ },
+
+ /*
+ label: clickable link text
+ func: the function that is called when clicked
+ prefix: some bit of text that precedes the label. (Used to show
+ counts here.)
+
+ */
+ makebtn: function(label, func, prefix){
+ var li = new Element('li');
+ var btn = new Element('a', {'html': label,
+ 'href': '#cpcspswdnbd',
+ 'events': {
+ // You might have to bind this differently
+ 'click': func.bind(this)
+ }
+ });
+ li.grab(btn);
+ if(prefix !== undefined){
+ prefix = new Element('span', {'text': prefix});
+ li.grab(prefix);
+ }
+ return li;
+ },
+
+ showtotal: function(){
+ // return true for every item in the datasrc
+ this.filter(this.options.datasrc.getChildren(),
+ function(item){
+ return true;
+ },
+ this.RESETINPUT
+ );
+ },
+ showselected: function(){
+ this.filter(this.options.datasrc.getChildren(),
+ function(item){
+ return (item.selected === true);
+ },
+ this.RESETINPUT
+ );
+ },
+ showunselected:function(){
+ this.filter(this.options.datasrc.getChildren(),
+ function(item){
+ return (item.selected !== true);
+ },
+ this.RESETINPUT
+ );
+ },
+ selectAll:function(){
+ this.options.datasrc.getChildren().each(
+ function(item){
+ item.selected = true;
+ }
+ );
+ this.fireEvent('rebuild', [this.options.datasrc.getChildren()]);
+ this.update(this.options.datasrc.getChildren().length);
+ },
+ selectNone:function(){
+ this.options.datasrc.getChildren().each(
+ function(item){
+ item.selected = false;
+ }
+ );
+ this.fireEvent('rebuild', [this.options.datasrc.getChildren()]);
+ this.update(0);
+ },
+
+ // list is the list of option dom elements from the select elem
+ // test is a function that gets used in the filter
+ filter: function(list, test, reset){
+ results = list.filter(function(item, index){
+ return test(item);
+ });
+
+ if(reset){
+ this.filterbox.value = "";
+ }
+ this.fireEvent('rebuild', [results]);
+ },
+ update: function(numselected){
+ var total = this.options.datasrc.getChildren().length;
+ this.totalbtn.getElement('span').set('text', total);
+ this.selectedbtn.getElement('span').set('text', numselected);
+ this.unselectedbtn.getElement('span').set('text', total -
numselected);
+ }
+});
+
+Element.Events.rebuild = {
+ 'base': 'change',
+ 'condition': function(evt){
+ return;
+ }
+};
+