changeset 6176:d25638d1826c

Add roundup-admin filter command; fix bad doc example; add tests admin_guide.txt had an example using find with username prop. This is wrong. Find only works with links not string. Fix it to use filter. Add filter command to roundup-admin. Add tests for filter, specification and find.
author John Rouillard <rouilj@ieee.org>
date Mon, 18 May 2020 23:28:03 -0400
parents 72a69753f49a
children 41907e1f9c3f
files CHANGES.txt doc/admin_guide.txt roundup/admin.py share/man/man1/roundup-admin.1 test/test_admin.py
diffstat 5 files changed, 215 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sat May 16 21:20:25 2020 -0400
+++ b/CHANGES.txt	Mon May 18 23:28:03 2020 -0400
@@ -41,6 +41,9 @@
 - Index created for documentation. Links created for website docs and
   released docs. Needs more refinement, but it exists at least.
   (John Rouillard)
+- New filter command defined in roundup-admin. (Partial fix for
+  issue724648.) (John Rouillard)
+
 
 2020-04-05 2.0.0 beta 0
 
--- a/doc/admin_guide.txt	Sat May 16 21:20:25 2020 -0400
+++ b/doc/admin_guide.txt	Mon May 18 23:28:03 2020 -0400
@@ -386,13 +386,14 @@
 
 (or if you know their username, and it happens to be "richard")::
 
-    roundup-admin find username=richard
+    roundup-admin filter user username=richard
 
-then using the user id you get from one of the above commands, you may
-display the user's details::
+then using the user id (e.g. 5) you get from one of the above
+commands, you may display the user's details::
 
-    roundup-admin display <userid>
+    roundup-admin display <designator>
 
+where designator is ``user5``.
 
 Running the Servers
 ===================
--- a/roundup/admin.py	Sat May 16 21:20:25 2020 -0400
+++ b/roundup/admin.py	Mon May 18 23:28:03 2020 -0400
@@ -718,6 +718,65 @@
         self.db_uncommitted = True
         return 0
 
+    def do_filter(self, args):
+        ''"""Usage: filter classname propname=value ...
+        Find the nodes of the given class with a given property value.
+
+        Find the nodes of the given class with a given property value.
+        Multiple values can be specified by separating them with commas.
+        If property is a string, all values must match. I.E. it's an
+        'and' operation. If the property is a link/multilink any value
+        matches. I.E. an 'or' operation.
+        """
+        if len(args) < 1:
+            raise UsageError(_('Not enough arguments supplied'))
+        classname = args[0]
+        # get the class
+        cl = self.get_class(classname)
+
+        # handle the propname=value argument
+        props = self.props_from_args(args[1:])
+
+        # convert the user-input value to a value used for filter
+        # multiple , separated values become a list
+        for propname, value in props.items():
+            if ',' in value:
+                values = value.split(',')
+            else:
+                values = value
+
+            props[propname] = values
+
+        # now do the filter
+        try:
+            id = []
+            designator = []
+            props = { "filterspec": props }
+
+            if self.separator:
+                if self.print_designator:
+                    id = cl.filter(None, **props)
+                    for i in id:
+                        designator.append(classname + i)
+                    print(self.separator.join(designator), file=sys.stdout)
+                else:
+                    print(self.separator.join(cl.find(**props)),
+                          file=sys.stdout)
+            else:
+                if self.print_designator:
+                    id = cl.filter(None, **props)
+                    for i in id:
+                        designator.append(classname + i)
+                    print(designator,file=sys.stdout)
+                else:
+                    print(cl.filter(None, **props), file=sys.stdout)
+        except KeyError:
+            raise UsageError(_('%(classname)s has no property '
+                               '"%(propname)s"') % locals())
+        except (ValueError, TypeError) as message:
+            raise UsageError(message)
+        return 0
+
     def do_find(self, args):
         ''"""Usage: find classname propname=value ...
         Find the nodes of the given class with a given link property value.
--- a/share/man/man1/roundup-admin.1	Sat May 16 21:20:25 2020 -0400
+++ b/share/man/man1/roundup-admin.1	Mon May 18 23:28:03 2020 -0400
@@ -89,6 +89,13 @@
 files below $TRACKER_HOME/db/files/ (which can be archived separately).
 To include the files, use the export command.
 .TP
+\fBfilter\fP \fIclassname propname=value ...\fP
+Find the nodes of the given class with a given property value.
+Multiple values can be specified by separating them with commas.
+If property is a string, all values must match. I.E. it's an
+'and' operation. If the property is a link/multilink any value
+matches. I.E. an 'or' operation.
+.TP
 \fBfind\fP \fIclassname propname=value ...\fP
 Find the nodes of the given class with a given link property value.
 .TP
--- a/test/test_admin.py	Sat May 16 21:20:25 2020 -0400
+++ b/test/test_admin.py	Mon May 18 23:28:03 2020 -0400
@@ -13,6 +13,24 @@
 from .test_mysql import skip_mysql
 from .test_postgresql import skip_postgresql
 
+# https://stackoverflow.com/questions/4219717/how-to-assert-output-with-nosetest-unittest-in-python
+# lightly modified
+from contextlib import contextmanager
+_py3 = sys.version_info[0] > 2
+if _py3:
+    from io import StringIO # py3
+else:
+    from StringIO import StringIO # py2
+
+@contextmanager
+def captured_output():
+    new_out, new_err = StringIO(), StringIO()
+    old_out, old_err = sys.stdout, sys.stderr
+    try:
+        sys.stdout, sys.stderr = new_out, new_err
+        yield sys.stdout, sys.stderr
+    finally:
+        sys.stdout, sys.stderr = old_out, old_err
 
 class AdminTest(object):
 
@@ -27,6 +45,27 @@
         except OSError as error:
             if error.errno not in (errno.ENOENT, errno.ESRCH): raise
 
+    def install_init(self, type="classic",
+                     settings="mail_domain=example.com," +
+                     "mail_host=localhost," + "tracker_web=http://test/" ):
+        ''' install tracker with settings for required config.ini settings.
+        '''
+
+        admin=AdminTool()
+
+        # Run under context manager to suppress output of help text.
+        with captured_output() as (out, err):
+            sys.argv=['main', '-i', '_test_admin', 'install',
+                      type, self.backend, settings ]
+            ret = admin.main()
+        self.assertEqual(ret, 0)
+
+        # initialize tracker with initial_data.py. Put password
+        # on cli so I don't have to respond to prompting.
+        sys.argv=['main', '-i', '_test_admin', 'initialise', 'admin']
+        ret = admin.main()
+        self.assertEqual(ret, 0)
+
     def testInit(self):
         import sys
         self.admin=AdminTool()
@@ -68,7 +107,109 @@
         self.assertTrue(os.path.isfile(self.dirname + "/schema.py"))
         config=CoreConfig(self.dirname)
         self.assertEqual(config['MAIL_DEBUG'], self.dirname + "/SendMail.LOG")
+
+    def testFind(self):
+        ''' Note the tests will fail if you run this under pdb.
+            the context managers capture the pdb prompts and this screws
+            up the stdout strings with (pdb) prefixed to the line.
+        '''
+        import sys, json
+
+        self.admin=AdminTool()
+        self.install_init()
+
+        with captured_output() as (out, err):
+            sys.argv=['main', '-i', '_test_admin', 'create', 'issue',
+                      'title="foo bar"', 'assignedto=admin' ]
+            ret = self.admin.main()
+
+        out = out.getvalue().strip()
+        print(out)
+        self.assertEqual(out, '1')
+
+        self.admin=AdminTool()
+        with captured_output() as (out, err):
+            sys.argv=['main', '-i', '_test_admin', 'create', 'issue',
+                      'title="bar foo bar"', 'assignedto=anonymous' ]
+            ret = self.admin.main()
+
+        out = out.getvalue().strip()
+        print(out)
+        self.assertEqual(out, '2')
+
+        self.admin=AdminTool()
+        with captured_output() as (out, err):
+            sys.argv=['main', '-i', '_test_admin', 'find', 'issue',
+                      'assignedto=1']
+            ret = self.admin.main()
+
+        out = out.getvalue().strip()
+        print(out)
+        self.assertEqual(out, "['1']")
+
+        # Reopen the db closed by previous filter call
+        self.admin=AdminTool()
+        with captured_output() as (out, err):
+            ''' 1,2 should return all entries that have assignedto
+                either admin or anonymous
+            '''
+            sys.argv=['main', '-i', '_test_admin', 'find', 'issue',
+                      'assignedto=1,2']
+            ret = self.admin.main()
+
+        out = out.getvalue().strip()
+        print(out)
+        # out can be "['2', '1']" or "['1', '2']"
+        # so eval to real list so Equal can do a list compare
+        self.assertEqual(sorted(eval(out)), ['1', '2'])
+
+        # Reopen the db closed by previous filter call
+        self.admin=AdminTool()
+        with captured_output() as (out, err):
+            ''' 1,2 should return all entries that have assignedto
+                either admin or anonymous
+            '''
+            sys.argv=['main', '-i', '_test_admin', 'find', 'issue',
+                      'assignedto=admin,anonymous']
+            ret = self.admin.main()
+
+        out = out.getvalue().strip()
+        print(out)
+        # out can be "['2', '1']" or "['1', '2']"
+        # so eval to real list so Equal can do a list compare
+        self.assertEqual(sorted(eval(out)), ['1', '2'])
+
+    def testSpecification(self):
+        ''' Note the tests will fail if you run this under pdb.
+            the context managers capture the pdb prompts and this screws
+            up the stdout strings with (pdb) prefixed to the line.
+        '''
+        import sys
+
+        self.install_init()
+        self.admin=AdminTool()
+
+        import inspect
         
+        spec='''username: <roundup.hyperdb.String> (key property)
+              alternate_addresses: <roundup.hyperdb.String>
+              realname: <roundup.hyperdb.String>
+              roles: <roundup.hyperdb.String>
+              organisation: <roundup.hyperdb.String>
+              queries: <roundup.hyperdb.Multilink to "query">
+              phone: <roundup.hyperdb.String>
+              address: <roundup.hyperdb.String>
+              timezone: <roundup.hyperdb.String>
+              password: <roundup.hyperdb.Password>'''
+
+        spec = inspect.cleandoc(spec)
+        with captured_output() as (out, err):
+            sys.argv=['main', '-i', '_test_admin', 'specification', 'user']
+            ret = self.admin.main()
+
+        out = out.getvalue().strip()
+        print(out)
+        self.assertEqual(out, spec)
 
 
 class anydbmAdminTest(AdminTest, unittest.TestCase):

Roundup Issue Tracker: http://roundup-tracker.org/