Index: src/sas/sasgui/perspectives/calculator/model_editor.py
===================================================================
--- src/sas/sasgui/perspectives/calculator/model_editor.py	(revision 23359ccb520c33d89444d0732d752a3bc0226916)
+++ src/sas/sasgui/perspectives/calculator/model_editor.py	(revision 23359ccb520c33d89444d0732d752a3bc0226916)
@@ -31,10 +31,13 @@
 import re
 import logging
+import datetime
+
 from wx.py.editwindow import EditWindow
+
 from sas.sasgui.guiframe.documentation_window import DocumentationWindow
+
 from .pyconsole import show_model_output, check_model
 
 logger = logging.getLogger(__name__)
-
 
 if sys.platform.count("win32") > 0:
@@ -78,5 +81,5 @@
     a Modal Dialog.
 
-    :TODO the build in compiler currently balks at when it tries to import
+    :TODO the built in compiler currently balks at when it tries to import
     a model whose name contains spaces or symbols (such as + ... underscore
     should be fine).  Have fixed so the editor cannot save such a file name
@@ -335,5 +338,5 @@
             list_fnames = os.listdir(self.plugin_dir)
             # fake existing regular model name list
-            m_list = [model + ".py" for model in self.model_list]
+            m_list = [model.name + ".py" for model in self.model_list]
             list_fnames.append(m_list)
             if t_fname in list_fnames and title != mname:
@@ -533,5 +536,5 @@
             desc_line = "\nmodel_info.description = '{}'".format(description)
         name = os.path.splitext(os.path.basename(self.fname))[0]
-        output = SUM_TEMPLATE.format(name=name, model1=model1_name, 
+        output = SUM_TEMPLATE.format(name=name, model1=model1_name,
             model2=model2_name, operator=self._operator, desc_line=desc_line)
         with open(self.fname, 'w') as out_f:
@@ -789,5 +792,5 @@
                     exec "float(math.%s)" % item
                     self.math_combo.Append(str(item))
-                except:
+                except Exception:
                     self.math_combo.Append(str(item) + "()")
         self.math_combo.Bind(wx.EVT_COMBOBOX, self._on_math_select)
@@ -914,14 +917,7 @@
             msg = "Name exists already."
 
-        # Prepare the messagebox
+        #
         if self.base is not None and not msg:
             self.base.update_custom_combo()
-            # Passed exception in import test as it will fail for sasmodels.sasview_model class
-            # Should add similar test for new style?
-            Model = None
-            try:
-                exec "from %s import Model" % name
-            except:
-                logger.error(sys.exc_value)
 
         # Prepare the messagebox
@@ -956,111 +952,73 @@
         :param func_str: content of func; Strings
         """
-        try:
-            out_f = open(fname, 'w')
-        except:
-            raise
-        # Prepare the content of the function
-        lines = CUSTOM_TEMPLATE.split('\n')
-
-        has_scipy = func_str.count("scipy.")
-        if has_scipy:
-            lines.insert(0, 'import scipy')
-
-        # Think about 2D later
-        #self.is_2d = func_str.count("#self.ndim = 2")
-        #line_2d = ''
-        #if self.is_2d:
-        #    line_2d = CUSTOM_2D_TEMP.split('\n')
-
-        # Also think about test later
-        #line_test = TEST_TEMPLATE.split('\n')
-        #local_params = ''
-        #spaces = '        '#8spaces
-        spaces4  = ' '*4
-        spaces13 = ' '*13
-        spaces16 = ' '*16
+        out_f = open(fname, 'w')
+
+        out_f.write(CUSTOM_TEMPLATE % {
+            'name': name,
+            'title': 'User model for ' + name,
+            'description': desc_str,
+            'date': datetime.datetime.now().strftime('%YYYY-%mm-%dd'),
+        })
+
+        # Write out parameters
         param_names = []    # to store parameter names
-        has_scipy = func_str.count("scipy.")
-        if has_scipy:
-            lines.insert(0, 'import scipy')
-
-        # write function here
-        for line in lines:
-            # The location where to put the strings is
-            # hard-coded in the template as shown below.
-            out_f.write(line + '\n')
-            if line.count('#name'):
-                out_f.write('name = "%s" \n' % name)
-            elif line.count('#title'):
-                out_f.write('title = "User model for %s"\n' % name)
-            elif line.count('#description'):
-                out_f.write('description = "%s"\n' % desc_str)
-            elif line.count('#parameters'):
-                out_f.write('parameters = [ \n')
-                for param_line in param_str.split('\n'):
-                    p_line = param_line.lstrip().rstrip()
-                    if p_line:
-                        pname, pvalue, desc = self.get_param_helper(p_line)
-                        param_names.append(pname)
-                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], '', '%s'],\n" % (spaces16, pname, pvalue, desc))
-                for param_line in pd_param_str.split('\n'):
-                    p_line = param_line.lstrip().rstrip()
-                    if p_line:
-                        pname, pvalue, desc = self.get_param_helper(p_line)
-                        param_names.append(pname)
-                        out_f.write("%s['%s', '', %s, [-numpy.inf, numpy.inf], 'volume', '%s'],\n" % (spaces16, pname, pvalue, desc))
-                out_f.write('%s]\n' % spaces13)
-
-        # No form_volume or ER available in simple model editor
-        out_f.write('def form_volume(*arg): \n')
-        out_f.write('    return 1.0 \n')
-        out_f.write('\n')
-        out_f.write('def ER(*arg): \n')
-        out_f.write('    return 1.0 \n')
-
-        # function to compute
-        out_f.write('\n')
-        out_f.write('def Iq(x ')
-        for name in param_names:
-            out_f.write(', %s' % name)
-        out_f.write('):\n')
+        pd_params = []
+        out_f.write('parameters = [ \n')
+        out_f.write('#   ["name", "units", default, [lower, upper], "type", "description"],\n')
+        for pname, pvalue, desc in self.get_param_helper(param_str):
+            param_names.append(pname)
+            out_f.write("    ['%s', '', %s, [-inf, inf], '', '%s'],\n"
+                        % (pname, pvalue, desc))
+        for pname, pvalue, desc in self.get_param_helper(pd_param_str):
+            param_names.append(pname)
+            pd_params.append(pname)
+            out_f.write("    ['%s', '', %s, [-inf, inf], 'volume', '%s'],\n"
+                        % (pname, pvalue, desc))
+        out_f.write('    ]\n')
+
+        # Write out function definition
+        out_f.write('def Iq(%s):\n' % ', '.join(['x'] + param_names))
+        out_f.write('    """Absolute scattering"""\n')
+        if "scipy." in func_str:
+            out_f.write('    import scipy')
+        if "numpy." in func_str:
+            out_f.write('    import numpy')
+        if "np." in func_str:
+            out_f.write('    import numpy as np')
         for func_line in func_str.split('\n'):
             out_f.write('%s%s\n' % (spaces4, func_line))
-
-        Iqxy_string = 'return Iq(numpy.sqrt(x**2+y**2) '
-
+        out_f.write('## uncomment the following if Iq works for vector x\n')
+        out_f.write('#Iq.vectorized = True\n')
+
+        # If polydisperse, create place holders for form_volume, ER and VR
+        if pd_params:
+            out_f.write('\n')
+            out_f.write(CUSTOM_TEMPLATE_PD % {'args': ', '.join(pd_params)})
+
+        # Create place holder for Iqxy
         out_f.write('\n')
-        out_f.write('def Iqxy(x, y ')
-        for name in param_names:
-            out_f.write(', %s' % name)
-            Iqxy_string += ', ' + name
-        out_f.write('):\n')
-        Iqxy_string += ')'
-        out_f.write('%s%s\n' % (spaces4, Iqxy_string))
+        out_f.write('#def Iqxy(%s):\n' % ', '.join(["x", "y"] + param_names))
+        out_f.write('#    """Absolute scattering of oriented particles."""\n')
+        out_f.write('#    ...\n')
+        out_f.write('#    return oriented_form(x, y, args)\n')
+        out_f.write('## uncomment the following if Iqxy works for vector x, y\n')
+        out_f.write('#Iqxy.vectorized = True\n')
 
         out_f.close()
 
-    def get_param_helper(self, line):
-        """
-        Get string in line to define the params dictionary
-
-        :param line: one line of string got from the param_str
-        """
-        items = line.split(";")
-        for item in items:
-            name = item.split("=")[0].strip()
-            description = ""
-            try:
-                value = item.split("=")[1].strip()
-                if value.count("#"):
-                    # If line ends in a comment, remove it before parsing float
-                    index = value.index("#")
-                    description = value[(index + 1):].strip()
-                    value = value[:value.index("#")].strip()
-                float(value)
-            except ValueError:
-                value = 1.0 # default
-
-        return name, value, description
+    def get_param_helper(self, param_str):
+        """
+        yield a sequence of name, value pairs for the parameters in param_str
+
+        Parameters can be defined by one per line by name=value, or multiple
+        on the same line by separating the pairs by semicolon or comma.  The
+        value is optional and defaults to "1.0".
+        """
+        for line in param_str.replace(';', ',').split('\n'):
+            for item in line.split(','):
+                defn, desc = item.split('#', 1) if '#' in item else (item, '')
+                name, value = defn.split('=', 1) if '=' in defn else (defn, '1.0')
+                if name:
+                    yield [v.strip() for v in (name, value, desc)]
 
     def set_function_helper(self, line):
@@ -1096,6 +1054,6 @@
         running "file:///...."
 
-    :param evt: Triggers on clicking the help button
-    """
+        :param evt: Triggers on clicking the help button
+        """
 
         _TreeLocation = "user/sasgui/perspectives/fitting/fitting_help.html"
@@ -1140,74 +1098,58 @@
 ## Templates for plugin models
 
-CUSTOM_TEMPLATE = """
+CUSTOM_TEMPLATE = '''\
+r"""
+Definition
+----------
+
+Calculates %(name)s.
+
+%(description)s
+
+References
+----------
+
+Authorship and Verification
+---------------------------
+
+* **Author:** --- **Date:** %(date)s
+* **Last Modified by:** --- **Date:** %(date)s
+* **Last Reviewed by:** --- **Date:** %(date)s
+"""
+
 from math import *
-import os
-import sys
-import numpy
-
-#name
-
-#title
-
-#description
-
-#parameters
-
-"""
-
-CUSTOM_2D_TEMP = """
-    def run(self, x=0.0, y=0.0):
-        if x.__class__.__name__ == 'list':
-            x_val = x[0]
-            y_val = y[0]*0.0
-            return self.function(x_val, y_val)
-        elif x.__class__.__name__ == 'tuple':
-            msg = "Tuples are not allowed as input to BaseComponent models"
-            raise ValueError, msg
-        else:
-            return self.function(x, 0.0)
-    def runXY(self, x=0.0, y=0.0):
-        if x.__class__.__name__ == 'list':
-            return self.function(x, y)
-        elif x.__class__.__name__ == 'tuple':
-            msg = "Tuples are not allowed as input to BaseComponent models"
-            raise ValueError, msg
-        else:
-            return self.function(x, y)
-    def evalDistribution(self, qdist):
-        if qdist.__class__.__name__ == 'list':
-            msg = "evalDistribution expects a list of 2 ndarrays"
-            if len(qdist)!=2:
-                raise RuntimeError, msg
-            if qdist[0].__class__.__name__ != 'ndarray':
-                raise RuntimeError, msg
-            if qdist[1].__class__.__name__ != 'ndarray':
-                raise RuntimeError, msg
-            v_model = numpy.vectorize(self.runXY, otypes=[float])
-            iq_array = v_model(qdist[0], qdist[1])
-            return iq_array
-        elif qdist.__class__.__name__ == 'ndarray':
-            v_model = numpy.vectorize(self.runXY, otypes=[float])
-            iq_array = v_model(qdist)
-            return iq_array
-"""
-TEST_TEMPLATE = """
-######################################################################
-## THIS IS FOR TEST. DO NOT MODIFY THE FOLLOWING LINES!!!!!!!!!!!!!!!!
-if __name__ == "__main__":
-    m= Model()
-    out1 = m.runXY(0.0)
-    out2 = m.runXY(0.01)
-    isfine1 = numpy.isfinite(out1)
-    isfine2 = numpy.isfinite(out2)
-    print "Testing the value at Q = 0.0:"
-    print out1, " : finite? ", isfine1
-    print "Testing the value at Q = 0.01:"
-    print out2, " : finite? ", isfine2
-    if isfine1 and isfine2:
-        print "===> Simple Test: Passed!"
-    else:
-        print "===> Simple Test: Failed!"
-"""
+from numpy import inf
+
+name = "%(name)s"
+title = "%(title)s"
+description = """%(description)s"""
+
+'''
+
+CUSTOM_TEMPLATE_PD = '''\
+def form_volume(%(args)s):
+    """
+    Volume of the particles used to compute absolute scattering intensity
+    and to weight polydisperse parameter contributions.
+    """
+    return 0.0
+
+def ER(%(args)s):
+    """
+    Effective radius of particles to be used when computing structure factors.
+
+    Input parameters are vectors ranging over the mesh of polydispersity values.
+    """
+    return 0.0
+
+def VR(%(args)s):
+    """
+    Volume ratio of particles to be used when computing structure factors.
+
+    Input parameters are vectors ranging over the mesh of polydispersity values.
+    """
+    return 1.0
+'''
+
 SUM_TEMPLATE = """
 from sasmodels.core import load_model_info
@@ -1219,15 +1161,7 @@
 """
 if __name__ == "__main__":
-#    app = wx.PySimpleApp()
     main_app = wx.App()
     main_frame = TextDialog(id=1, model_list=["SphereModel", "CylinderModel"],
-                       plugin_dir='../fitting/plugin_models')
+                            plugin_dir='../fitting/plugin_models')
     main_frame.ShowModal()
     main_app.MainLoop()
-
-#if __name__ == "__main__":
-#    from sas.sasgui.perspectives.fitting import models
-#    dir_path = models.find_plugins_dir()
-#    app = wx.App()
-#    window = EditorWindow(parent=None, base=None, path=dir_path, title="Editor")
-#    app.MainLoop()
