Skip to content

Commit 26dd014

Browse files
committed
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.
1 parent f184096 commit 26dd014

File tree

5 files changed

+215
-4
lines changed

5 files changed

+215
-4
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ Features:
4141
- Index created for documentation. Links created for website docs and
4242
released docs. Needs more refinement, but it exists at least.
4343
(John Rouillard)
44+
- New filter command defined in roundup-admin. (Partial fix for
45+
issue724648.) (John Rouillard)
46+
4447

4548
2020-04-05 2.0.0 beta 0
4649

doc/admin_guide.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,13 +386,14 @@ you can just display another user::
386386

387387
(or if you know their username, and it happens to be "richard")::
388388

389-
roundup-admin find username=richard
389+
roundup-admin filter user username=richard
390390

391-
then using the user id you get from one of the above commands, you may
392-
display the user's details::
391+
then using the user id (e.g. 5) you get from one of the above
392+
commands, you may display the user's details::
393393

394-
roundup-admin display <userid>
394+
roundup-admin display <designator>
395395

396+
where designator is ``user5``.
396397

397398
Running the Servers
398399
===================

roundup/admin.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,65 @@ def do_set(self, args):
718718
self.db_uncommitted = True
719719
return 0
720720

721+
def do_filter(self, args):
722+
''"""Usage: filter classname propname=value ...
723+
Find the nodes of the given class with a given property value.
724+
725+
Find the nodes of the given class with a given property value.
726+
Multiple values can be specified by separating them with commas.
727+
If property is a string, all values must match. I.E. it's an
728+
'and' operation. If the property is a link/multilink any value
729+
matches. I.E. an 'or' operation.
730+
"""
731+
if len(args) < 1:
732+
raise UsageError(_('Not enough arguments supplied'))
733+
classname = args[0]
734+
# get the class
735+
cl = self.get_class(classname)
736+
737+
# handle the propname=value argument
738+
props = self.props_from_args(args[1:])
739+
740+
# convert the user-input value to a value used for filter
741+
# multiple , separated values become a list
742+
for propname, value in props.items():
743+
if ',' in value:
744+
values = value.split(',')
745+
else:
746+
values = value
747+
748+
props[propname] = values
749+
750+
# now do the filter
751+
try:
752+
id = []
753+
designator = []
754+
props = { "filterspec": props }
755+
756+
if self.separator:
757+
if self.print_designator:
758+
id = cl.filter(None, **props)
759+
for i in id:
760+
designator.append(classname + i)
761+
print(self.separator.join(designator), file=sys.stdout)
762+
else:
763+
print(self.separator.join(cl.find(**props)),
764+
file=sys.stdout)
765+
else:
766+
if self.print_designator:
767+
id = cl.filter(None, **props)
768+
for i in id:
769+
designator.append(classname + i)
770+
print(designator,file=sys.stdout)
771+
else:
772+
print(cl.filter(None, **props), file=sys.stdout)
773+
except KeyError:
774+
raise UsageError(_('%(classname)s has no property '
775+
'"%(propname)s"') % locals())
776+
except (ValueError, TypeError) as message:
777+
raise UsageError(message)
778+
return 0
779+
721780
def do_find(self, args):
722781
''"""Usage: find classname propname=value ...
723782
Find the nodes of the given class with a given link property value.

share/man/man1/roundup-admin.1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ Export the database to colon-separated-value files, excluding the
8989
files below $TRACKER_HOME/db/files/ (which can be archived separately).
9090
To include the files, use the export command.
9191
.TP
92+
\fBfilter\fP \fIclassname propname=value ...\fP
93+
Find the nodes of the given class with a given property value.
94+
Multiple values can be specified by separating them with commas.
95+
If property is a string, all values must match. I.E. it's an
96+
'and' operation. If the property is a link/multilink any value
97+
matches. I.E. an 'or' operation.
98+
.TP
9299
\fBfind\fP \fIclassname propname=value ...\fP
93100
Find the nodes of the given class with a given link property value.
94101
.TP

test/test_admin.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@
1313
from .test_mysql import skip_mysql
1414
from .test_postgresql import skip_postgresql
1515

16+
# https://stackoverflow.com/questions/4219717/how-to-assert-output-with-nosetest-unittest-in-python
17+
# lightly modified
18+
from contextlib import contextmanager
19+
_py3 = sys.version_info[0] > 2
20+
if _py3:
21+
from io import StringIO # py3
22+
else:
23+
from StringIO import StringIO # py2
24+
25+
@contextmanager
26+
def captured_output():
27+
new_out, new_err = StringIO(), StringIO()
28+
old_out, old_err = sys.stdout, sys.stderr
29+
try:
30+
sys.stdout, sys.stderr = new_out, new_err
31+
yield sys.stdout, sys.stderr
32+
finally:
33+
sys.stdout, sys.stderr = old_out, old_err
1634

1735
class AdminTest(object):
1836

@@ -27,6 +45,27 @@ def tearDown(self):
2745
except OSError as error:
2846
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
2947

48+
def install_init(self, type="classic",
49+
settings="mail_domain=example.com," +
50+
"mail_host=localhost," + "tracker_web=http://test/" ):
51+
''' install tracker with settings for required config.ini settings.
52+
'''
53+
54+
admin=AdminTool()
55+
56+
# Run under context manager to suppress output of help text.
57+
with captured_output() as (out, err):
58+
sys.argv=['main', '-i', '_test_admin', 'install',
59+
type, self.backend, settings ]
60+
ret = admin.main()
61+
self.assertEqual(ret, 0)
62+
63+
# initialize tracker with initial_data.py. Put password
64+
# on cli so I don't have to respond to prompting.
65+
sys.argv=['main', '-i', '_test_admin', 'initialise', 'admin']
66+
ret = admin.main()
67+
self.assertEqual(ret, 0)
68+
3069
def testInit(self):
3170
import sys
3271
self.admin=AdminTool()
@@ -68,7 +107,109 @@ def testInitWithConfig_ini(self):
68107
self.assertTrue(os.path.isfile(self.dirname + "/schema.py"))
69108
config=CoreConfig(self.dirname)
70109
self.assertEqual(config['MAIL_DEBUG'], self.dirname + "/SendMail.LOG")
110+
111+
def testFind(self):
112+
''' Note the tests will fail if you run this under pdb.
113+
the context managers capture the pdb prompts and this screws
114+
up the stdout strings with (pdb) prefixed to the line.
115+
'''
116+
import sys, json
117+
118+
self.admin=AdminTool()
119+
self.install_init()
120+
121+
with captured_output() as (out, err):
122+
sys.argv=['main', '-i', '_test_admin', 'create', 'issue',
123+
'title="foo bar"', 'assignedto=admin' ]
124+
ret = self.admin.main()
125+
126+
out = out.getvalue().strip()
127+
print(out)
128+
self.assertEqual(out, '1')
129+
130+
self.admin=AdminTool()
131+
with captured_output() as (out, err):
132+
sys.argv=['main', '-i', '_test_admin', 'create', 'issue',
133+
'title="bar foo bar"', 'assignedto=anonymous' ]
134+
ret = self.admin.main()
135+
136+
out = out.getvalue().strip()
137+
print(out)
138+
self.assertEqual(out, '2')
139+
140+
self.admin=AdminTool()
141+
with captured_output() as (out, err):
142+
sys.argv=['main', '-i', '_test_admin', 'find', 'issue',
143+
'assignedto=1']
144+
ret = self.admin.main()
145+
146+
out = out.getvalue().strip()
147+
print(out)
148+
self.assertEqual(out, "['1']")
149+
150+
# Reopen the db closed by previous filter call
151+
self.admin=AdminTool()
152+
with captured_output() as (out, err):
153+
''' 1,2 should return all entries that have assignedto
154+
either admin or anonymous
155+
'''
156+
sys.argv=['main', '-i', '_test_admin', 'find', 'issue',
157+
'assignedto=1,2']
158+
ret = self.admin.main()
159+
160+
out = out.getvalue().strip()
161+
print(out)
162+
# out can be "['2', '1']" or "['1', '2']"
163+
# so eval to real list so Equal can do a list compare
164+
self.assertEqual(sorted(eval(out)), ['1', '2'])
165+
166+
# Reopen the db closed by previous filter call
167+
self.admin=AdminTool()
168+
with captured_output() as (out, err):
169+
''' 1,2 should return all entries that have assignedto
170+
either admin or anonymous
171+
'''
172+
sys.argv=['main', '-i', '_test_admin', 'find', 'issue',
173+
'assignedto=admin,anonymous']
174+
ret = self.admin.main()
175+
176+
out = out.getvalue().strip()
177+
print(out)
178+
# out can be "['2', '1']" or "['1', '2']"
179+
# so eval to real list so Equal can do a list compare
180+
self.assertEqual(sorted(eval(out)), ['1', '2'])
181+
182+
def testSpecification(self):
183+
''' Note the tests will fail if you run this under pdb.
184+
the context managers capture the pdb prompts and this screws
185+
up the stdout strings with (pdb) prefixed to the line.
186+
'''
187+
import sys
188+
189+
self.install_init()
190+
self.admin=AdminTool()
191+
192+
import inspect
71193

194+
spec='''username: <roundup.hyperdb.String> (key property)
195+
alternate_addresses: <roundup.hyperdb.String>
196+
realname: <roundup.hyperdb.String>
197+
roles: <roundup.hyperdb.String>
198+
organisation: <roundup.hyperdb.String>
199+
queries: <roundup.hyperdb.Multilink to "query">
200+
phone: <roundup.hyperdb.String>
201+
address: <roundup.hyperdb.String>
202+
timezone: <roundup.hyperdb.String>
203+
password: <roundup.hyperdb.Password>'''
204+
205+
spec = inspect.cleandoc(spec)
206+
with captured_output() as (out, err):
207+
sys.argv=['main', '-i', '_test_admin', 'specification', 'user']
208+
ret = self.admin.main()
209+
210+
out = out.getvalue().strip()
211+
print(out)
212+
self.assertEqual(out, spec)
72213

73214

74215
class anydbmAdminTest(AdminTest, unittest.TestCase):

0 commit comments

Comments
 (0)