• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

allura


Commit MetaInfo

Revisionf8cf1c7537a01f86631d1f6bff3eaae3341ec6d7 (tree)
Time2012-07-12 01:01:47
AuthorTim Van Steenburgh <tvansteenburgh@gmai...>
CommiterTim Van Steenburgh

Log Message

Merge branch 'db/4272' into dev

Change Summary

Incremental Difference

--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -577,10 +577,14 @@ class Ticket(VersionedArtifact, ActivityObject):
577577 custom_fields=self.custom_fields)
578578
579579 @classmethod
580- def paged_query(cls, query, limit=None, page=0, sort=None, columns=None, **kw):
581- """Query tickets, sorting and paginating the result."""
580+ def paged_query(cls, app_config, user, query, limit=None, page=0, sort=None, **kw):
581+ """
582+ Query tickets, filtering for 'read' permission, sorting and paginating the result.
583+
584+ See also paged_search which does a solr search
585+ """
582586 limit, page, start = g.handle_paging(limit, page, default=25)
583- q = cls.query.find(dict(query, app_config_id=c.app.config._id))
587+ q = cls.query.find(dict(query, app_config_id=app_config._id))
584588 q = q.sort('ticket_num')
585589 if sort:
586590 field, direction = sort.split()
@@ -595,27 +599,76 @@ class Ticket(VersionedArtifact, ActivityObject):
595599 tickets = []
596600 count = q.count()
597601 for t in q:
598- if security.has_access(t, 'read'):
602+ if security.has_access(t, 'read', user, app_config.project):
599603 tickets.append(t)
600604 else:
601605 count = count -1
602- sortable_custom_fields=c.app.globals.sortable_custom_fields_shown_in_search()
603- if not columns:
604- columns = [dict(name='ticket_num', sort_name='ticket_num', label='Ticket Number', active=True),
605- dict(name='summary', sort_name='summary', label='Summary', active=True),
606- dict(name='_milestone', sort_name='custom_fields._milestone', label='Milestone', active=True),
607- dict(name='status', sort_name='status', label='Status', active=True),
608- dict(name='assigned_to', sort_name='assigned_to_username', label='Owner', active=True)]
609- for field in sortable_custom_fields:
610- columns.append(
611- dict(name=field['name'], sort_name=field['name'], label=field['label'], active=True))
612606 return dict(
613607 tickets=tickets,
614- sortable_custom_fields=sortable_custom_fields,
615- columns=columns,
616608 count=count, q=json.dumps(query), limit=limit, page=page, sort=sort,
617609 **kw)
618610
611+ @classmethod
612+ def paged_search(cls, app_config, user, q, limit=None, page=0, sort=None, **kw):
613+ """Query tickets from Solr, filtering for 'read' permission, sorting and paginating the result.
614+
615+ See also paged_query which does a mongo search.
616+
617+ We do the sorting and skipping right in SOLR, before we ever ask
618+ Mongo for the actual tickets. Other keywords for
619+ search_artifact (e.g., history) or for SOLR are accepted through
620+ kw. The output is intended to be used directly in templates,
621+ e.g., exposed controller methods can just:
622+
623+ return paged_query(q, ...)
624+
625+ If you want all the results at once instead of paged you have
626+ these options:
627+ - don't call this routine, search directly in mongo
628+ - call this routine with a very high limit and TEST that
629+ count<=limit in the result
630+ limit=-1 is NOT recognized as 'all'. 500 is a reasonable limit.
631+ """
632+
633+ limit, page, start = g.handle_paging(limit, page, default=25)
634+ count = 0
635+ tickets = []
636+ refined_sort = sort if sort else 'ticket_num_i asc'
637+ if 'ticket_num_i' not in refined_sort:
638+ refined_sort += ',ticket_num_i asc'
639+ try:
640+ if q:
641+ matches = search_artifact(
642+ cls, q,
643+ rows=limit, sort=refined_sort, start=start, fl='ticket_num_i', **kw)
644+ else:
645+ matches = None
646+ solr_error = None
647+ except ValueError, e:
648+ solr_error = e.args[0]
649+ matches = []
650+ if matches:
651+ count = matches.hits
652+ # ticket_numbers is in sorted order
653+ ticket_numbers = [match['ticket_num_i'] for match in matches.docs]
654+ # but query, unfortunately, returns results in arbitrary order
655+ query = cls.query.find(dict(app_config_id=app_config._id, ticket_num={'$in':ticket_numbers}))
656+ # so stick all the results in a dictionary...
657+ ticket_for_num = {}
658+ for t in query:
659+ ticket_for_num[t.ticket_num] = t
660+ # and pull them out in the order given by ticket_numbers
661+ tickets = []
662+ for tn in ticket_numbers:
663+ if tn in ticket_for_num:
664+ if security.has_access(ticket_for_num[tn], 'read', user, app_config.project):
665+ tickets.append(ticket_for_num[tn])
666+ else:
667+ count = count -1
668+ return dict(tickets=tickets,
669+ count=count, q=q, limit=limit, page=page, sort=sort,
670+ solr_error=solr_error, **kw)
671+
619672 class TicketAttachment(BaseAttachment):
620673 thumbnail_size = (100, 100)
621674 ArtifactType=Ticket
--- a/ForgeTracker/forgetracker/tests/functional/test_rest.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_rest.py
@@ -3,10 +3,14 @@ pylons.c = pylons.tmpl_context
33 pylons.g = pylons.app_globals
44 from pylons import c
55
6+from datadiff.tools import assert_equal
7+from mock import patch
8+
69 from allura.lib import helpers as h
710 from allura.tests import decorators as td
811 from alluratest.controller import TestRestApiBase
912
13+from forgetracker import model as TM
1014
1115 class TestTrackerApiBase(TestRestApiBase):
1216
@@ -60,7 +64,7 @@ class TestRestUpdateTicket(TestTrackerApiBase):
6064 self.ticket_args = ticket_view.json['ticket']
6165
6266 def test_ticket_index(self):
63- tickets = self.api_post('/rest/p/test/bugs/')
67+ tickets = self.api_get('/rest/p/test/bugs/')
6468 assert len(tickets.json['tickets']) == 1, tickets.json
6569 assert (tickets.json['tickets'][0]
6670 == dict(ticket_num=1, summary='test new ticket')), tickets.json['tickets'][0]
@@ -94,22 +98,55 @@ class TestRestDiscussion(TestTrackerApiBase):
9498 self.ticket_args = ticket_view.json['ticket']
9599
96100 def test_index(self):
97- r = self.api_post('/rest/p/test/bugs/_discuss/')
101+ r = self.api_get('/rest/p/test/bugs/_discuss/')
98102 assert len(r.json['discussion']['threads']) == 1, r.json
99103 for t in r.json['discussion']['threads']:
100- r = self.api_post('/rest/p/test/bugs/_discuss/thread/%s/' % t['_id'])
104+ r = self.api_get('/rest/p/test/bugs/_discuss/thread/%s/' % t['_id'])
101105 assert len(r.json['thread']['posts']) == 0, r.json
102106
103107 def test_post(self):
104- discussion = self.api_post('/rest/p/test/bugs/_discuss/').json['discussion']
108+ discussion = self.api_get('/rest/p/test/bugs/_discuss/').json['discussion']
105109 post = self.api_post('/rest/p/test/bugs/_discuss/thread/%s/new' % discussion['threads'][0]['_id'],
106110 text='This is a comment', wrap_args=None)
107- thread = self.api_post('/rest/p/test/bugs/_discuss/thread/%s/' % discussion['threads'][0]['_id'])
111+ thread = self.api_get('/rest/p/test/bugs/_discuss/thread/%s/' % discussion['threads'][0]['_id'])
108112 assert len(thread.json['thread']['posts']) == 1, thread.json
109113 assert post.json['post']['text'] == 'This is a comment', post.json
110114 reply = self.api_post(
111115 '/rest/p/test/bugs/_discuss/thread/%s/%s/reply' % (thread.json['thread']['_id'], post.json['post']['slug']),
112116 text='This is a reply', wrap_args=None)
113117 assert reply.json['post']['text'] == 'This is a reply', reply.json
114- thread = self.api_post('/rest/p/test/bugs/_discuss/thread/%s/' % discussion['threads'][0]['_id'])
118+ thread = self.api_get('/rest/p/test/bugs/_discuss/thread/%s/' % discussion['threads'][0]['_id'])
115119 assert len(thread.json['thread']['posts']) == 2, thread.json
120+
121+class TestRestSearch(TestTrackerApiBase):
122+
123+ @patch('forgetracker.model.Ticket.paged_search')
124+ def test_no_criteria(self, paged_search):
125+ paged_search.return_value = dict(tickets=[
126+ TM.Ticket(ticket_num=5, summary='our test ticket'),
127+ ])
128+ r = self.api_get('/rest/p/test/bugs/search')
129+ assert_equal(r.status_int, 200)
130+ assert_equal(r.json, {'tickets':[
131+ {'summary': 'our test ticket', 'ticket_num': 5},
132+ ]})
133+
134+ @patch('forgetracker.model.Ticket.paged_search')
135+ def test_some_criteria(self, paged_search):
136+ q = 'labels:testing && status:open'
137+ paged_search.return_value = dict(tickets=[
138+ TM.Ticket(ticket_num=5, summary='our test ticket'),
139+ ],
140+ sort='status',
141+ limit=2,
142+ count=1,
143+ page=0,
144+ q=q,
145+ )
146+ r = self.api_get('/rest/p/test/bugs/search', q=q, sort='status', limit='2')
147+ assert_equal(r.status_int, 200)
148+ assert_equal(r.json, {'limit': 2, 'q': q, 'sort':'status', 'count': 1,
149+ 'page': 0, 'tickets':[
150+ {'summary': 'our test ticket', 'ticket_num': 5},
151+ ]
152+ })
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -163,12 +163,21 @@ class TestFunctionalController(TrackerTestController):
163163 r = self.app.get('/p/test/bugs/search/?q=ticket', extra_environ=env)
164164 assert '1 results' in r
165165 assert 'Private Ticket' not in r
166+ # ... or in search feed...
167+ r = self.app.get('/p/test/bugs/search_feed?q=ticket', extra_environ=env)
168+ assert 'Private Ticket' not in r
166169 # ...and can't get to the private ticket directly.
167170 r = self.app.get(ticket_view.request.url, extra_environ=env)
168171 assert 'Private Ticket' not in r
169172 # ... and it doesn't appear in the feed
170173 r = self.app.get('/p/test/bugs/feed.atom')
171174 assert 'Private Ticket' not in r
175+ # ... or in the API ...
176+ r = self.app.get('/rest/p/test/bugs/2/')
177+ assert 'Private Ticket' not in r
178+ assert '/auth/?return_to' in r.headers['Location']
179+ r = self.app.get('/rest/p/test/bugs/')
180+ assert 'Private Ticket' not in r
172181
173182 @td.with_tool('test', 'Tickets', 'doc-bugs')
174183 def test_two_trackers(self):
@@ -659,6 +668,16 @@ class TestFunctionalController(TrackerTestController):
659668 assert '3 results' in response, response.showbrowser()
660669 assert 'test third ticket' in response, response.showbrowser()
661670
671+ def test_search_feed(self):
672+ self.new_ticket(summary='test first ticket')
673+ ThreadLocalORMSession.flush_all()
674+ M.MonQTask.run_ready()
675+ ThreadLocalORMSession.flush_all()
676+ response = self.app.get('/p/test/bugs/search_feed?q=test')
677+ assert '<title>test first ticket</title>' in response
678+ response = self.app.get('/p/test/bugs/search_feed.atom?q=test')
679+ assert '<title>test first ticket</title>' in response
680+
662681 def test_touch(self):
663682 self.new_ticket(summary='test touch')
664683 h.set_context('test', 'bugs', neighborhood='Projects')
--- a/ForgeTracker/forgetracker/tests/test_controller.py
+++ /dev/null
@@ -1,12 +0,0 @@
1-from nose.tools import assert_true
2-
3-from allura.tests import decorators as td
4-from alluratest.controller import TestController
5-
6-
7-class TestRootController(TestController):
8- @td.with_tracker
9- def test_index(self):
10- response = self.app.get('/bugs/')
11- assert_true('bugs' in response)
12-
--- a/ForgeTracker/forgetracker/tests/unit/test_root_controller.py
+++ b/ForgeTracker/forgetracker/tests/unit/test_root_controller.py
@@ -49,7 +49,7 @@ def solr_search_returning_colors_are_wrong_ticket():
4949 matches = Mock()
5050 matches.docs = [dict(ticket_num_i=ticket.ticket_num)]
5151 search_artifact.return_value = matches
52- return patch('forgetracker.tracker_main.search_artifact', search_artifact)
52+ return patch('forgetracker.model.ticket.search_artifact', search_artifact)
5353
5454 def mongo_search_returning_colors_are_wrong_ticket():
5555 ticket = create_colors_are_wrong_ticket()
@@ -82,4 +82,3 @@ def create_ticket(summary, custom_fields):
8282 custom_fields=custom_fields)
8383 session(ticket).flush()
8484 return ticket
85-
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -306,6 +306,33 @@ class ForgeTrackerApp(Application):
306306 def bins(self):
307307 return TM.Bin.query.find(dict(app_config_id=self.config._id)).sort('summary').all()
308308
309+
310+
311+
312+
313+### Controllers ###
314+
315+def mongo_columns():
316+ columns = [dict(name='ticket_num', sort_name='ticket_num', label='Ticket Number', active=True),
317+ dict(name='summary', sort_name='summary', label='Summary', active=True),
318+ dict(name='_milestone', sort_name='custom_fields._milestone', label='Milestone', active=True),
319+ dict(name='status', sort_name='status', label='Status', active=True),
320+ dict(name='assigned_to', sort_name='assigned_to_username', label='Owner', active=True)]
321+ for field in c.app.globals.sortable_custom_fields_shown_in_search():
322+ columns.append(
323+ dict(name=field['name'], sort_name=field['name'], label=field['label'], active=True))
324+ return columns
325+
326+def solr_columns():
327+ columns = [dict(name='ticket_num', sort_name='ticket_num_i', label='Ticket Number', active=True),
328+ dict(name='summary', sort_name='snippet_s', label='Summary', active=True),
329+ dict(name='_milestone', sort_name='_milestone_s', label='Milestone', active=True),
330+ dict(name='status', sort_name='status_s', label='Status', active=True),
331+ dict(name='assigned_to', sort_name='assigned_to_s', label='Owner', active=True)]
332+ for field in c.app.globals.sortable_custom_fields_shown_in_search():
333+ columns.append(dict(name=field['name'], sort_name=field['sortable_name'], label=field['label'], active=True))
334+ return columns
335+
309336 class RootController(BaseController):
310337
311338 def __init__(self):
@@ -331,84 +358,17 @@ class RootController(BaseController):
331358 bin_counts.append(dict(label=label, count=count))
332359 return dict(bin_counts=bin_counts)
333360
334- def paged_query(self, q, limit=None, page=0, sort=None, columns=None, **kw):
335- """Query tickets, sorting and paginating the result.
336-
337- We do the sorting and skipping right in SOLR, before we ever ask
338- Mongo for the actual tickets. Other keywords for
339- search_artifact (e.g., history) or for SOLR are accepted through
340- kw. The output is intended to be used directly in templates,
341- e.g., exposed controller methods can just:
342-
343- return paged_query(q, ...)
344-
345- If you want all the results at once instead of paged you have
346- these options:
347- - don't call this routine, search directly in mongo
348- - call this routine with a very high limit and TEST that
349- count<=limit in the result
350- limit=-1 is NOT recognized as 'all'. 500 is a reasonable limit.
351- """
352-
353- limit, page, start = g.handle_paging(limit, page, default=25)
354- count = 0
355- tickets = []
356- refined_sort = sort if sort else 'ticket_num_i asc'
357- if 'ticket_num_i' not in refined_sort:
358- refined_sort += ',ticket_num_i asc'
359- try:
360- if q:
361- matches = search_artifact(
362- TM.Ticket, q,
363- rows=limit, sort=refined_sort, start=start, fl='ticket_num_i', **kw)
364- else:
365- matches = None
366- solr_error = None
367- except ValueError, e:
368- solr_error = e.args[0]
369- matches = []
370- if matches:
371- count = matches.hits
372- # ticket_numbers is in sorted order
373- ticket_numbers = [match['ticket_num_i'] for match in matches.docs]
374- # but query, unfortunately, returns results in arbitrary order
375- query = TM.Ticket.query.find(dict(app_config_id=c.app.config._id, ticket_num={'$in':ticket_numbers}))
376- # so stick all the results in a dictionary...
377- ticket_for_num = {}
378- for t in query:
379- ticket_for_num[t.ticket_num] = t
380- # and pull them out in the order given by ticket_numbers
381- tickets = []
382- for tn in ticket_numbers:
383- if tn in ticket_for_num:
384- if has_access(ticket_for_num[tn], 'read'):
385- tickets.append(ticket_for_num[tn])
386- else:
387- count = count -1
388- sortable_custom_fields=c.app.globals.sortable_custom_fields_shown_in_search()
389- if not columns:
390- columns = [dict(name='ticket_num', sort_name='ticket_num_i', label='Ticket Number', active=True),
391- dict(name='summary', sort_name='snippet_s', label='Summary', active=True),
392- dict(name='_milestone', sort_name='_milestone_s', label='Milestone', active=True),
393- dict(name='status', sort_name='status_s', label='Status', active=True),
394- dict(name='assigned_to', sort_name='assigned_to_s', label='Owner', active=True)]
395- for field in sortable_custom_fields:
396- columns.append(dict(name=field['name'], sort_name=field['sortable_name'], label=field['label'], active=True))
397- return dict(tickets=tickets,
398- sortable_custom_fields=sortable_custom_fields,
399- columns=columns,
400- count=count, q=q, limit=limit, page=page, sort=sort,
401- solr_error=solr_error, **kw)
402-
403361 @with_trailing_slash
404362 @h.vardec
405363 @expose('jinja:forgetracker:templates/tracker/index.html')
406364 def index(self, limit=25, columns=None, page=0, sort='ticket_num desc', **kw):
407365 kw.pop('q', None) # it's just our original query mangled and sent back to us
408- result = TM.Ticket.paged_query(c.app.globals.not_closed_mongo_query,
366+ result = TM.Ticket.paged_query(c.app.config, c.user,
367+ c.app.globals.not_closed_mongo_query,
409368 sort=sort, limit=int(limit),
410- columns=columns, page=page, **kw)
411- c.subscribe_form = W.subscribe_form
369+ page=page, **kw)
370+ result['columns'] = columns or mongo_columns()
371+ result['sortable_custom_fields'] = c.app.globals.sortable_custom_fields_shown_in_search()
412372 result['subscribed'] = M.Mailbox.subscribed()
413373 result['allow_edit'] = has_access(c.app, 'update')()
414374 result['help_msg'] = c.app.config.options.get('TicketHelpSearch')
@@ -418,6 +378,7 @@ class RootController(BaseController):
418378 sort_split = sort.split(' ')
419379 solr_col = _mongo_col_to_solr_col(sort_split[0])
420380 result['url_sort'] = '%s %s' % (solr_col, sort_split[1])
381+ c.subscribe_form = W.subscribe_form
421382 c.ticket_search_results = W.ticket_search_results
422383 return result
423384
@@ -506,7 +467,9 @@ class RootController(BaseController):
506467 bin = TM.Bin.query.find(dict(app_config_id=c.app.config._id,terms=q)).first()
507468 if project:
508469 redirect(c.project.url() + 'search?' + urlencode(dict(q=q, history=kw.get('history'))))
509- result = self.paged_query(q, page=page, sort=sort, columns=columns, **kw)
470+ result = TM.Ticket.paged_search(c.app.config, c.user, q, page=page, sort=sort, **kw)
471+ result['columns'] = columns or solr_columns()
472+ result['sortable_custom_fields'] = c.app.globals.sortable_custom_fields_shown_in_search()
510473 result['allow_edit'] = has_access(c.app, 'update')()
511474 result['bin'] = bin
512475 result['help_msg'] = c.app.config.options.get('TicketHelpSearch')
@@ -517,23 +480,24 @@ class RootController(BaseController):
517480 @h.vardec
518481 @expose()
519482 @validate(validators=search_validators)
520- def search_feed(self, q=None, query=None, project=None, columns=None, page=0, sort=None, **kw):
483+ def search_feed(self, q=None, query=None, project=None, page=0, sort=None, **kw):
521484 if query and not q:
522485 q = query
523- result = self.paged_query(q, page=page, sort=sort, columns=columns, **kw)
486+ result = TM.Ticket.paged_search(c.app.config, c.user, q, page=page, sort=sort, **kw)
524487 response.headers['Content-Type'] = ''
525488 response.content_type = 'application/xml'
526- d = dict(title='Ticket search results', link=c.app.url, description='You searched for %s' % q, language=u'en')
489+ d = dict(title='Ticket search results', link=h.absurl(c.app.url), description='You searched for %s' % q, language=u'en')
527490 if request.environ['PATH_INFO'].endswith('.atom'):
528491 feed = FG.Atom1Feed(**d)
529492 else:
530493 feed = FG.Rss201rev2Feed(**d)
531494 for t in result['tickets']:
495+ url = h.absurl(t.url().encode('utf-8'))
532496 feed.add_item(title=t.summary,
533- link=h.absurl(t.url().encode('utf-8')),
497+ link=url,
534498 pubdate=t.mod_date,
535499 description=t.description,
536- unique_id=str(t._id),
500+ unique_id=url,
537501 author_name=t.reported_by.display_name,
538502 author_link=h.absurl(t.reported_by.url()))
539503 return feed.writeString('utf-8')
@@ -622,9 +586,11 @@ class RootController(BaseController):
622586 sort=validators.UnicodeString(if_empty='ticket_num_i asc')))
623587 def edit(self, q=None, limit=None, page=None, sort=None, **kw):
624588 require_access(c.app, 'update')
625- result = self.paged_query(q, sort=sort, limit=limit, page=page, **kw)
589+ result = TM.Ticket.paged_search(c.app.config, c.user, q, sort=sort, limit=limit, page=page, **kw)
626590 # if c.app.globals.milestone_names is None:
627591 # c.app.globals.milestone_names = ''
592+ result['columns'] = solr_columns()
593+ result['sortable_custom_fields'] = c.app.globals.sortable_custom_fields_shown_in_search()
628594 result['globals'] = c.app.globals
629595 result['cancel_href'] = url(
630596 c.app.url + 'search/',
@@ -1338,10 +1304,14 @@ class RootRestController(BaseController):
13381304 require_access(c.app, 'read')
13391305
13401306 @expose('json:')
1341- def index(self, **kw):
1342- return dict(tickets=[
1343- dict(ticket_num=t.ticket_num, summary=t.summary)
1344- for t in TM.Ticket.query.find(dict(app_config_id=c.app.config._id)).sort('ticket_num') ])
1307+ def index(self, limit=100, page=0, **kw):
1308+ results = TM.Ticket.paged_query(c.app.config, c.user, query={},
1309+ limit=int(limit), page=int(page))
1310+ results['tickets'] = [dict(ticket_num=t.ticket_num, summary=t.summary)
1311+ for t in results['tickets']]
1312+ results.pop('q', None)
1313+ results.pop('sort', None)
1314+ return results
13451315
13461316 @expose()
13471317 @h.vardec
@@ -1385,6 +1355,13 @@ class RootRestController(BaseController):
13851355 log.exception(e)
13861356 return dict(status=False, errors=[str(e)])
13871357
1358+ @expose('json:')
1359+ def search(self, q=None, limit=100, page=0, sort=None, **kw):
1360+ results = TM.Ticket.paged_search(c.app.config, c.user, q, limit, page, sort)
1361+ results['tickets'] = [dict(ticket_num=t.ticket_num, summary=t.summary)
1362+ for t in results['tickets']]
1363+ return results
1364+
13881365 @expose()
13891366 def _lookup(self, ticket_num, *remainder):
13901367 return TicketRestController(ticket_num), remainder
@@ -1445,8 +1422,10 @@ class MilestoneController(BaseController):
14451422 sort=validators.UnicodeString(if_empty=None)))
14461423 def index(self, q=None, columns=None, page=0, query=None, sort=None, **kw):
14471424 require(has_access(c.app, 'read'))
1448- result = TM.Ticket.paged_query(
1449- self.mongo_query, page=page, sort=sort, columns=columns, **kw)
1425+ result = TM.Ticket.paged_query(c.app.config, c.user,
1426+ self.mongo_query, page=page, sort=sort, **kw)
1427+ result['columns'] = columns or mongo_columns()
1428+ result['sortable_custom_fields'] = c.app.globals.sortable_custom_fields_shown_in_search()
14501429 result['allow_edit'] = has_access(c.app, 'update')()
14511430 result['help_msg'] = c.app.config.options.get('TicketHelpSearch')
14521431 progress = c.app.globals.milestone_count(self.progress_key)
@@ -1474,8 +1453,10 @@ class MilestoneController(BaseController):
14741453 sort=validators.UnicodeString(if_empty='ticket_num_i asc')))
14751454 def edit(self, q=None, limit=None, page=None, sort=None, columns=None, **kw):
14761455 require_access(c.app, 'update')
1477- result = TM.Ticket.paged_query(
1478- self.mongo_query, page=page, sort=sort, columns=columns, **kw)
1456+ result = TM.Ticket.paged_query(c.app.config, c.user,
1457+ self.mongo_query, page=page, sort=sort, **kw)
1458+ result['columns'] = columns or mongo_columns()
1459+ result['sortable_custom_fields'] = c.app.globals.sortable_custom_fields_shown_in_search()
14791460 # if c.app.globals.milestone_names is None:
14801461 # c.app.globals.milestone_names = ''
14811462 result.pop('q')