[GRASS-SVN] r41886 - in grass/trunk/gui/wxpython: . gui_modules xml
svn_grass at osgeo.org
svn_grass at osgeo.org
Fri Apr 16 14:23:40 EDT 2010
Author: martinl
Date: 2010-04-16 14:23:39 -0400 (Fri, 16 Apr 2010)
New Revision: 41886
Modified:
grass/trunk/gui/wxpython/gui_modules/gcmd.py
grass/trunk/gui/wxpython/gui_modules/gmodeler.py
grass/trunk/gui/wxpython/gui_modules/goutput.py
grass/trunk/gui/wxpython/wxgui.py
grass/trunk/gui/wxpython/xml/menudata.xml
Log:
wxGUI: run model from menu
Modified: grass/trunk/gui/wxpython/gui_modules/gcmd.py
===================================================================
--- grass/trunk/gui/wxpython/gui_modules/gcmd.py 2010-04-16 12:40:00 UTC (rev 41885)
+++ grass/trunk/gui/wxpython/gui_modules/gcmd.py 2010-04-16 18:23:39 UTC (rev 41886)
@@ -91,27 +91,29 @@
reason, exception),
caption = caption,
style = style)
-
+
+class GError(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return str(self.value)
+
class GException(Exception):
"""!Generic exception"""
- def __init__(self, message, title=_("Error"), parent=None):
+ def __init__(self, message, title = _("Error"), parent = None):
self.msg = message
self.parent = parent
self.title = title
def Show(self):
- dlg = wx.MessageDialog(parent=self.parent,
- caption=self.title,
- message=self.msg,
- style=wx.ICON_ERROR | wx.CENTRE)
- dlg.SetIcon(wx.Icon(os.path.join(globalvar.ETCICONDIR, 'grass_error.ico'), wx.BITMAP_TYPE_ICO))
- if self.parent:
- dlg.CentreOnParent()
- else:
- dlg.CentreOnScreen()
+ GMessage(parent = self.parent,
+ message = self.msg,
+ msgType = 'error')
- dlg.ShowModal()
-
+ def GetMessage(self):
+ return self.msg
+
def __str__(self):
self.Show()
@@ -120,9 +122,9 @@
class GStdError(GException):
"""!Generic exception"""
- def __init__(self, message, title=_("Error"), parent=None):
+ def __init__(self, message, title = _("Error"), parent = None):
GException.__init__(self, message, title=title, parent=parent)
-
+
class CmdError(GException):
"""!Exception used for GRASS commands.
Modified: grass/trunk/gui/wxpython/gui_modules/gmodeler.py
===================================================================
--- grass/trunk/gui/wxpython/gui_modules/gmodeler.py 2010-04-16 12:40:00 UTC (rev 41885)
+++ grass/trunk/gui/wxpython/gui_modules/gmodeler.py 2010-04-16 18:23:39 UTC (rev 41886)
@@ -4,6 +4,7 @@
@brief Graphical modeler to create edit, and manage models
Classes:
+ - Model
- ModelFrame
- ModelCanvas
- ModelAction
@@ -52,10 +53,185 @@
import gselect
from debug import Debug
from gcmd import GMessage
+from gcmd import GError
from gdialogs import ElementDialog
from gdialogs import GetImageHandlers
from grass.script import core as grass
+class Model(object):
+ """!Class representing the model"""
+ def __init__(self, canvas = None):
+ self.actions = list() # list of recorded actions
+ self.data = list() # list of recorded data items
+ self.canvas = canvas
+
+ def GetCanvas(self):
+ """!Get canvas or None"""
+ return self.canvas
+
+ def GetActions(self):
+ """!Return list of actions"""
+ return self.actions
+
+ def GetData(self):
+ """!Return list of data"""
+ return self.data
+
+ def Reset(self):
+ """!Reset model"""
+ self.actions = list()
+ self.data = list()
+
+ def AddAction(self, item):
+ """!Add action to the model"""
+ self.actions.append(item)
+
+ def AddData(self, item):
+ """!Add data to the model"""
+ self.data.append(item)
+
+ def FindAction(self, id):
+ """!Find action by id"""
+ for action in self.actions:
+ if action.GetId() == id:
+ return action
+
+ return None
+
+ def FindData(self, value, prompt):
+ """!Find data by value, and prompt"""
+ for data in self.data:
+ if data.GetValue() == value and \
+ data.GetPrompt() == prompt:
+ return data
+
+ return None
+
+ def LoadModel(self, filename):
+ """!Load model definition stored in GRASS Model XML file (gxm)
+
+ @todo Validate against DTD
+
+ Raise exception on error.
+ """
+ dtdFilename = os.path.join(globalvar.ETCWXDIR, "xml", "grass-gxm.dtd")
+
+ # parse workspace file
+ try:
+ gxmXml = ProcessModelFile(etree.parse(filename))
+ except StandardError, e:
+ raise GError(e)
+
+ # load model.GetActions()
+ for action in gxmXml.actions:
+ actionItem = ModelAction(parent = self,
+ x = action['pos'][0],
+ y = action['pos'][1],
+ width = action['size'][0],
+ height = action['size'][1],
+ cmd = action['cmd'])
+ actionItem.SetId(action['id'])
+
+ self.actions.append(actionItem)
+
+ task = menuform.GUI().ParseCommand(cmd = actionItem.GetLog(string = False),
+ show = None)
+ valid = True
+ for p in task.get_options()['params']:
+ if p.get('value', '') == '' and \
+ p.get('default', '') == '':
+ valid = False
+ break
+ actionItem.SetValid(valid)
+
+ # load data & connections
+ for data in gxmXml.data:
+ dataItem = ModelData(parent = self,
+ x = data['pos'][0],
+ y = data['pos'][1],
+ width = data['size'][0],
+ height = data['size'][1],
+ name = data['name'],
+ prompt = data['prompt'],
+ value = data['value'])
+ dataItem.SetIntermediate(data['intermediate'])
+
+ for idx in range(len(data['id'])):
+ actionItem = self.FindAction(data['id'][idx])
+ if data['from'][idx] is True:
+ dataItem.AddAction(actionItem, direction = 'from')
+ elif data['from'][idx] is False:
+ dataItem.AddAction(actionItem, direction = 'to')
+
+ self.data.append(dataItem)
+
+ actionItem.AddData(dataItem)
+
+ def IsValid(self):
+ """Return True if model is valid"""
+ if self.Validate():
+ return False
+
+ return True
+
+ def Validate(self):
+ """!Validate model, return None if model is valid otherwise
+ error string"""
+ errList = list()
+ for action in self.actions:
+ task = menuform.GUI().ParseCommand(cmd = action.GetLog(string = False),
+ show = None)
+ errList += task.getCmdError()
+
+ return errList
+
+ def Run(self, log, onDone):
+ """!Run model"""
+ for action in self.actions:
+ log.RunCmd(command = action.GetLog(string = False),
+ onDone = onDone)
+
+ def DeleteIntermediateData(self, log):
+ """!Detele intermediate data"""
+ rast, vect, rast3d, msg = self.GetIntermediateData()
+
+ if rast:
+ log.RunCmd(['g.remove', 'rast=%s' %','.join(rast)])
+ if rast3d:
+ log.RunCmd(['g.remove', 'rast3d=%s' %','.join(rast3d)])
+ if vect:
+ log.RunCmd(['g.remove', 'vect=%s' %','.join(vect)])
+
+ def GetIntermediateData(self):
+ """!Get info about intermediate data"""
+ rast = list()
+ rast3d = list()
+ vect = list()
+ for data in self.data:
+ if not data.IsIntermediate():
+ continue
+ name = data.GetValue()
+ prompt = data.GetPrompt()
+ if prompt == 'raster':
+ rast.append(name)
+ elif prompt == 'vector':
+ vect.append(name)
+ elif prompt == 'rast3d':
+ rast3d.append(name)
+
+ msg = ''
+ if rast:
+ msg += '\n\n%s: ' % _('Raster maps')
+ msg += ', '.join(rast)
+ if rast3d:
+ msg += '\n\n%s: ' % _('3D raster maps')
+ msg += ', '.join(rast3d)
+ if vect:
+ msg += '\n\n%s: ' % _('Vector maps')
+ msg += ', '.join(vect)
+
+ return rast, vect, rast3d, msg
+
class ModelFrame(wx.Frame):
def __init__(self, parent, id = wx.ID_ANY, title = _("Graphical modeler (under development)"), **kwargs):
"""!Graphical modeler main window
@@ -68,8 +244,6 @@
"""
self.parent = parent
self.searchDialog = None # module search dialog
- self.actions = list() # list of recorded actions
- self.data = list() # list of recorded data items
self.baseTitle = title
self.modelFile = None # loaded model
self.modelChanged = False
@@ -89,9 +263,9 @@
self.toolbar = toolbars.ModelToolbar(parent = self)
self.SetToolBar(self.toolbar)
-
+
self.statusbar = self.CreateStatusBar(number = 1)
-
+
self.notebook = FN.FlatNotebook(parent = self, id = wx.ID_ANY,
style = FN.FNB_FANCY_TABS | FN.FNB_BOTTOM |
FN.FNB_NO_NAV_BUTTONS | FN.FNB_NO_X_BUTTON)
@@ -100,6 +274,8 @@
self.canvas.SetBackgroundColour(wx.WHITE)
self.canvas.SetCursor(self.cursors["default"])
+ self.model = Model(self.canvas)
+
self.goutput = goutput.GMConsole(parent = self, pageid = 1)
self.modelPage = self.notebook.AddPage(self.canvas, text=_('Model'))
@@ -136,24 +312,7 @@
evthandler.SetShape(item)
evthandler.SetPreviousHandler(item.GetEventHandler())
item.SetEventHandler(evthandler)
-
- def FindAction(self, id):
- """!Find action by id"""
- for action in self.actions:
- if action.GetId() == id:
- return action
- return None
-
- def FindData(self, value, prompt):
- """!Find data by value, and prompt"""
- for data in self.data:
- if data.GetValue() == value and \
- data.GetPrompt() == prompt:
- return data
-
- return None
-
def ModelChanged(self):
"""!Update window title"""
if not self.modelChanged:
@@ -168,43 +327,19 @@
def OnDeleteData(self, event):
"""!Delete intermediate data"""
- rast = list()
- rast3d = list()
- vect = list()
- for data in self.data:
- if not data.IsIntermediate():
- continue
- name = data.GetValue()
- prompt = data.GetPrompt()
- if prompt == 'raster':
- rast.append(name)
- elif prompt == 'vector':
- vect.append(name)
- elif prompt == 'rast3d':
- rast3d.append(name)
-
+ rast, vect, rast3d, msg = self.model.GetIntermediateData()
+
if not rast and not vect and not rast3d:
GMessage(parent = self,
message = _('Nothing to delete.'),
msgType = 'info')
return
-
- msg = ''
- if rast:
- msg += '\n\n%s: ' % _('Raster maps')
- msg += ', '.join(rast)
- if rast3d:
- msg += '\n\n%s: ' % _('3D raster maps')
- msg += ', '.join(rast3d)
- if vect:
- msg += '\n\n%s: ' % _('Vector maps')
- msg += ', '.join(vect)
dlg = wx.MessageDialog(parent = self,
message= _("Do you want to permanently delete data?%s" % msg),
caption=_("Delete intermediate data?"),
style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
-
+
ret = dlg.ShowModal()
if ret == wx.ID_YES:
dlg.Destroy()
@@ -230,7 +365,7 @@
if self.modelFile and self.modelChanged:
self.OnModelSave()
elif self.modelFile is None and \
- (len(self.actions) > 0 or len(self.data) > 0):
+ (len(self.model.GetActions()) > 0 or len(self.model.GetData()) > 0):
dlg = wx.MessageDialog(self, message=_("Current model is not empty. "
"Do you want to store current settings "
"to model file?"),
@@ -248,8 +383,7 @@
# delete all items
self.canvas.GetDiagram().DeleteAllShapes()
- self.actions = list()
- self.data = list()
+ self.model.Reset()
self.canvas.Refresh()
# no model file loaded
@@ -278,7 +412,7 @@
self.modelFile = filename
self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
- self.SetStatusText(_('%d actions loaded into model') % len(self.actions), 0)
+ self.SetStatusText(_('%d actions loaded into model') % len(self.model.GetActions()), 0)
def OnModelSave(self, event = None):
"""!Save model to file"""
@@ -341,7 +475,7 @@
if self.modelFile and self.modelChanged:
self.OnModelSave()
elif self.modelFile is None and \
- (len(self.actions) > 0 or len(self.data) > 0):
+ (len(self.model.GetActions()) > 0 or len(self.model.GetData()) > 0):
dlg = wx.MessageDialog(self, message=_("Current model is not empty. "
"Do you want to store current settings "
"to model file?"),
@@ -361,14 +495,13 @@
self.SetTitle(self.baseTitle)
self.canvas.GetDiagram().DeleteAllShapes()
- self.actions = list()
- self.data = list()
+ self.model.Reset()
self.canvas.Refresh()
def OnRunModel(self, event):
"""!Run entire model"""
- if len(self.actions) < 1:
+ if len(self.model.GetActions()) < 1:
GMessage(parent = self,
message = _('Model is empty. Nothing to run.'),
msgType = 'info')
@@ -386,8 +519,8 @@
if ret != wx.ID_YES:
return
- for action in self.actions:
- self.SetStatusText(_('Running model...'), 0)
+ for action in self.model.GetActions():
+ self.SetStatusText(_('Running model...'), 0)
self.goutput.RunCmd(command = action.GetLog(string = False),
onDone = self.OnDone)
@@ -397,7 +530,7 @@
def OnValidateModel(self, event, showMsg = True):
"""!Validate entire model"""
- if len(self.actions) < 1:
+ if len(self.model.GetActions()) < 1:
GMessage(parent = self,
message = _('Model is empty. Nothing to validate.'),
msgType = 'info')
@@ -552,7 +685,7 @@
""")
fd.write("\ndef main():\n")
- for action in self.actions:
+ for action in self.model.GetActions():
task = menuform.GUI().ParseCommand(cmd = action.GetLog(string = False),
show = None)
opts = task.get_options()
@@ -596,12 +729,9 @@
def _validateModel(self):
"""!Validate model"""
self.SetStatusText(_('Validating model...'), 0)
- errList = list()
- for action in self.actions:
- task = menuform.GUI().ParseCommand(cmd = action.GetLog(string = False),
- show = None)
- errList += task.getCmdError()
+ errList = self.model.Validate()
+
self.SetStatusText('', 0)
return errList
@@ -638,7 +768,7 @@
action.Show(True)
self._addEvent(action)
- self.actions.append(action)
+ self.model.AddAction(action)
self.canvas.Refresh()
time.sleep(.1)
@@ -663,7 +793,7 @@
data.Show(True)
self._addEvent(data)
- self.data.append(data)
+ self.model.AddData(data)
self.canvas.Refresh()
@@ -736,7 +866,7 @@
data.Show(True)
self._addEvent(data)
- self.data.append(data)
+ self.model.AddData(data)
if p.get('age', 'old') == 'old':
self._addLine(data, layer)
@@ -783,21 +913,13 @@
def LoadModelFile(self, filename):
"""!Load model definition stored in GRASS Model XML file (gxm)
-
- @todo Validate against DTD
-
- Raise exception on error.
"""
- ### dtdFilename = os.path.join(globalvar.ETCWXDIR, "xml", "grass-gxm.dtd")
-
- # parse workspace file
try:
- gxmXml = ProcessModelFile(etree.parse(filename))
- except:
+ self.model.LoadModel(filename)
+ except GError, e:
GMessage(parent = self,
message = _("Reading model file <%s> failed.\n"
"Invalid file, unable to parse XML document.") % filename)
- return
self.modelFile = filename
self.SetTitle(self.baseTitle + " - " + os.path.basename(self.modelFile))
@@ -805,58 +927,21 @@
self.SetStatusText(_("Please wait, loading model..."), 0)
# load actions
- for action in gxmXml.actions:
- actionShape = ModelAction(parent = self,
- x = action['pos'][0],
- y = action['pos'][1],
- width = action['size'][0],
- height = action['size'][1],
- cmd = action['cmd'])
- actionShape.SetId(action['id'])
- self.canvas.diagram.AddShape(actionShape)
- actionShape.Show(True)
-
- self._addEvent(actionShape)
- self.actions.append(actionShape)
-
- task = menuform.GUI().ParseCommand(cmd = actionShape.GetLog(string = False),
- show = None)
- valid = True
- for p in task.get_options()['params']:
- if p.get('value', '') == '' and \
- p.get('default', '') == '':
- valid = False
- break
- actionShape.SetValid(valid)
+ for action in self.model.GetActions():
+ self._addEvent(action)
+ self.canvas.diagram.AddShape(action)
+ action.Show(True)
- # load data & connections
- for data in gxmXml.data:
- dataShape = ModelData(parent = self,
- x = data['pos'][0],
- y = data['pos'][1],
- width = data['size'][0],
- height = data['size'][1],
- name = data['name'],
- prompt = data['prompt'],
- value = data['value'])
- dataShape.SetIntermediate(data['intermediate'])
-
- self.canvas.diagram.AddShape(dataShape)
- dataShape.Show(True)
-
- self._addEvent(dataShape)
- self.data.append(dataShape)
+ # load data & relations
+ for data in self.model.GetData():
+ self._addEvent(data)
+ self.canvas.diagram.AddShape(data)
+ data.Show(True)
- for idx in range(len(data['id'])):
- actionShape = self.FindAction(data['id'][idx])
- if data['from'][idx] is True:
- self._addLine(dataShape, actionShape)
- dataShape.AddAction(actionShape, direction = 'from')
- elif data['from'][idx] is False:
- self._addLine(actionShape, dataShape)
- dataShape.AddAction(actionShape, direction = 'to')
-
- actionShape.AddData(dataShape)
+ for action in data.GetActions('from'):
+ self._addLine(data, action)
+ for action in data.GetActions('to'):
+ self._addLine(action, data)
self.SetStatusText('', 0)
@@ -878,7 +963,7 @@
return False
try:
- WriteModelFile(fd = file, actions = self.actions, data = self.data)
+ WriteModelFile(fd = file, actions = self.model.GetActions(), data = self.model.GetData())
except StandardError:
file.close()
@@ -924,19 +1009,19 @@
self.colors['valid'] = wx.LIGHT_GREY_BRUSH
self.colors['invalid'] = wx.WHITE_BRUSH
- ogl.RectangleShape.__init__(self, width, height)
+ if self.parent.GetCanvas():
+ ogl.RectangleShape.__init__(self, width, height)
+
+ self.SetCanvas(self.parent)
+ self.SetX(x)
+ self.SetY(y)
+ self.SetPen(wx.BLACK_PEN)
+ self.SetBrush(self.colors['invalid'])
+ if self.cmd and len(self.cmd) > 0:
+ self.AddText(self.cmd[0])
+ else:
+ self.AddText('<<module>>')
- # self.Draggable(True)
- self.SetCanvas(self.parent)
- self.SetX(x)
- self.SetY(y)
- self.SetPen(wx.BLACK_PEN)
- self.SetBrush(self.colors['invalid'])
- if self.cmd and len(self.cmd) > 0:
- self.AddText(self.cmd[0])
- else:
- self.AddText('<<module>>')
-
def GetId(self):
"""!Get id"""
return self.id
@@ -1012,31 +1097,31 @@
self.propWin = None
self.actions = { 'from' : list(), 'to' : list() }
+
+ if self.parent.GetCanvas():
+ ogl.EllipseShape.__init__(self, width, height)
+
+ self.SetCanvas(self.parent)
+ self.SetX(x)
+ self.SetY(y)
+ self.SetPen(wx.BLACK_PEN)
+ if self.prompt == 'raster':
+ self.SetBrush(wx.Brush(wx.Colour(215, 215, 248)))
+ elif self.prompt == 'vector':
+ self.SetBrush(wx.Brush(wx.Colour(248, 215, 215)))
+ else:
+ self.SetBrush(wx.LIGHT_GREY_BRUSH)
- ogl.EllipseShape.__init__(self, width, height)
+ if name:
+ self.AddText(name)
+ else:
+ self.AddText(_('unknown'))
- # self.Draggable(True)
- self.SetCanvas(self.parent)
- self.SetX(x)
- self.SetY(y)
- self.SetPen(wx.BLACK_PEN)
- if self.prompt == 'raster':
- self.SetBrush(wx.Brush(wx.Colour(215, 215, 248)))
- elif self.prompt == 'vector':
- self.SetBrush(wx.Brush(wx.Colour(248, 215, 215)))
- else:
- self.SetBrush(wx.LIGHT_GREY_BRUSH)
+ if value:
+ self.AddText(value)
+ else:
+ self.AddText('\n')
- if name:
- self.AddText(name)
- else:
- self.AddText(_('unknown'))
-
- if value:
- self.AddText(value)
- else:
- self.AddText('\n')
-
def IsIntermediate(self):
"""!Checks if data item is intermediate"""
return self.intermediate
@@ -1263,15 +1348,21 @@
self.frame.canvas.Refresh()
def OnRemove(self, event):
- """!Remove shape"""
+ """!Remove shape
+
+ @todo complex remove
+ """
self.frame.ModelChanged()
shapes = [self.GetShape()]
- if isinstance(shape, ModelAction):
- pass
- if isinstance(shape, ModelData):
- pass
for shape in shapes:
+ if isinstance(shape, ModelAction):
+ pass
+ if isinstance(shape, ModelData):
+ pass
+
+ shape.Select(False)
self.frame.canvas.GetDiagram().RemoveShape(shape)
+
self.frame.canvas.Refresh()
class ModelSearchDialog(wx.Dialog):
Modified: grass/trunk/gui/wxpython/gui_modules/goutput.py
===================================================================
--- grass/trunk/gui/wxpython/gui_modules/goutput.py 2010-04-16 12:40:00 UTC (rev 41885)
+++ grass/trunk/gui/wxpython/gui_modules/goutput.py 2010-04-16 18:23:39 UTC (rev 41886)
@@ -686,6 +686,7 @@
pass
self.btn_abort.Enable(False)
+
if event.onDone:
event.onDone(returncode = event.returncode)
Modified: grass/trunk/gui/wxpython/wxgui.py
===================================================================
--- grass/trunk/gui/wxpython/wxgui.py 2010-04-16 12:40:00 UTC (rev 41885)
+++ grass/trunk/gui/wxpython/wxgui.py 2010-04-16 18:23:39 UTC (rev 41886)
@@ -266,6 +266,44 @@
win.Show()
+ def OnDone(self, returncode):
+ """Command execution finised"""
+ if hasattr(self, "model"):
+ self.model.DeleteIntermediateData(log = self.goutput)
+ del self.model
+ self.SetStatusText('')
+
+ def OnRunModel(self, event):
+ """!Run model"""
+ filename = ''
+ dlg = wx.FileDialog(parent = self, message=_("Choose model to run"),
+ defaultDir = os.getcwd(),
+ wildcard=_("GRASS Model File (*.gxm)|*.gxm"))
+ if dlg.ShowModal() == wx.ID_OK:
+ filename = dlg.GetPath()
+
+ if not filename:
+ return
+
+ self.model = gmodeler.Model()
+ self.model.LoadModel(filename)
+ self.SetStatusText(_('Validating model...'), 0)
+ result = self.model.Validate()
+ if result:
+ dlg = wx.MessageDialog(parent = self,
+ message = _('Model is not valid. Do you want to '
+ 'run the model anyway?\n\n%s') % '\n'.join(errList),
+ caption=_("Run model?"),
+ style = wx.YES_NO | wx.NO_DEFAULT |
+ wx.ICON_QUESTION | wx.CENTRE)
+ ret = dlg.ShowModal()
+ if ret != wx.ID_YES:
+ return
+
+ self.SetStatusText(_('Running model...'), 0)
+ self.model.Run(log = self.goutput,
+ onDone = self.OnDone)
+
def OnMapsets(self, event):
"""
Launch mapset access dialog
Modified: grass/trunk/gui/wxpython/xml/menudata.xml
===================================================================
--- grass/trunk/gui/wxpython/xml/menudata.xml 2010-04-16 12:40:00 UTC (rev 41885)
+++ grass/trunk/gui/wxpython/xml/menudata.xml 2010-04-16 18:23:39 UTC (rev 41886)
@@ -624,6 +624,11 @@
<help>Launch Graphical modeler</help>
<handler>OnGModeler</handler>
</menuitem>
+ <menuitem>
+ <label>Run model</label>
+ <help>Run model prepared by Graphical modeler</help>
+ <handler>OnRunModel</handler>
+ </menuitem>
<separator />
<menuitem>
<label>NVIZ (requires Tcl/Tk)</label>
More information about the grass-commit
mailing list