• 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

Revisiónff74de6ff9840e44229fa8dbf8272e63189eb624 (tree)
Tiempo2012-07-11 12:45:16
AutorDave Brondsema <dbrondsema@geek...>
CommiterDave Brondsema

Log Message

[#4272] add rest API for ticket searches; refactor paged_search to better name & location

Cambiar Resumen

Diferencia incremental

--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -578,7 +578,11 @@ class Ticket(VersionedArtifact, ActivityObject):
578578
579579 @classmethod
580580 def paged_query(cls, query, limit=None, page=0, sort=None, columns=None, **kw):
581- """Query tickets, sorting and paginating the result."""
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)
583587 q = cls.query.find(dict(query, app_config_id=c.app.config._id))
584588 q = q.sort('ticket_num')
@@ -616,6 +620,78 @@ class Ticket(VersionedArtifact, ActivityObject):
616620 count=count, q=json.dumps(query), limit=limit, page=page, sort=sort,
617621 **kw)
618622
623+ @classmethod
624+ def paged_search(cls, q, limit=None, page=0, sort=None, columns=None, **kw):
625+ """Query tickets from Solr, filtering for 'read' permission, sorting and paginating the result.
626+
627+ See also paged_query which does a mongo search.
628+
629+ We do the sorting and skipping right in SOLR, before we ever ask
630+ Mongo for the actual tickets. Other keywords for
631+ search_artifact (e.g., history) or for SOLR are accepted through
632+ kw. The output is intended to be used directly in templates,
633+ e.g., exposed controller methods can just:
634+
635+ return paged_query(q, ...)
636+
637+ If you want all the results at once instead of paged you have
638+ these options:
639+ - don't call this routine, search directly in mongo
640+ - call this routine with a very high limit and TEST that
641+ count<=limit in the result
642+ limit=-1 is NOT recognized as 'all'. 500 is a reasonable limit.
643+ """
644+
645+ limit, page, start = g.handle_paging(limit, page, default=25)
646+ count = 0
647+ tickets = []
648+ refined_sort = sort if sort else 'ticket_num_i asc'
649+ if 'ticket_num_i' not in refined_sort:
650+ refined_sort += ',ticket_num_i asc'
651+ try:
652+ if q:
653+ matches = search_artifact(
654+ cls, q,
655+ rows=limit, sort=refined_sort, start=start, fl='ticket_num_i', **kw)
656+ else:
657+ matches = None
658+ solr_error = None
659+ except ValueError, e:
660+ solr_error = e.args[0]
661+ matches = []
662+ if matches:
663+ count = matches.hits
664+ # ticket_numbers is in sorted order
665+ ticket_numbers = [match['ticket_num_i'] for match in matches.docs]
666+ # but query, unfortunately, returns results in arbitrary order
667+ query = cls.query.find(dict(app_config_id=c.app.config._id, ticket_num={'$in':ticket_numbers}))
668+ # so stick all the results in a dictionary...
669+ ticket_for_num = {}
670+ for t in query:
671+ ticket_for_num[t.ticket_num] = t
672+ # and pull them out in the order given by ticket_numbers
673+ tickets = []
674+ for tn in ticket_numbers:
675+ if tn in ticket_for_num:
676+ if security.has_access(ticket_for_num[tn], 'read'):
677+ tickets.append(ticket_for_num[tn])
678+ else:
679+ count = count -1
680+ sortable_custom_fields=c.app.globals.sortable_custom_fields_shown_in_search()
681+ if not columns:
682+ columns = [dict(name='ticket_num', sort_name='ticket_num_i', label='Ticket Number', active=True),
683+ dict(name='summary', sort_name='snippet_s', label='Summary', active=True),
684+ dict(name='_milestone', sort_name='_milestone_s', label='Milestone', active=True),
685+ dict(name='status', sort_name='status_s', label='Status', active=True),
686+ dict(name='assigned_to', sort_name='assigned_to_s', label='Owner', active=True)]
687+ for field in sortable_custom_fields:
688+ columns.append(dict(name=field['name'], sort_name=field['sortable_name'], label=field['label'], active=True))
689+ return dict(tickets=tickets,
690+ sortable_custom_fields=sortable_custom_fields,
691+ columns=columns,
692+ count=count, q=q, limit=limit, page=page, sort=sort,
693+ solr_error=solr_error, **kw)
694+
619695 class TicketAttachment(BaseAttachment):
620696 thumbnail_size = (100, 100)
621697 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
@@ -113,3 +117,36 @@ class TestRestDiscussion(TestTrackerApiBase):
113117 assert reply.json['post']['text'] == 'This is a reply', reply.json
114118 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/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
@@ -331,75 +331,6 @@ class RootController(BaseController):
331331 bin_counts.append(dict(label=label, count=count))
332332 return dict(bin_counts=bin_counts)
333333
334- def paged_query(self, q, limit=None, page=0, sort=None, columns=None, **kw):
335- """Query tickets, filtering for 'read' permission, 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-
403334 @with_trailing_slash
404335 @h.vardec
405336 @expose('jinja:forgetracker:templates/tracker/index.html')
@@ -506,7 +437,7 @@ class RootController(BaseController):
506437 bin = TM.Bin.query.find(dict(app_config_id=c.app.config._id,terms=q)).first()
507438 if project:
508439 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)
440+ result = TM.Ticket.paged_search(q, page=page, sort=sort, columns=columns, **kw)
510441 result['allow_edit'] = has_access(c.app, 'update')()
511442 result['bin'] = bin
512443 result['help_msg'] = c.app.config.options.get('TicketHelpSearch')
@@ -520,7 +451,7 @@ class RootController(BaseController):
520451 def search_feed(self, q=None, query=None, project=None, columns=None, page=0, sort=None, **kw):
521452 if query and not q:
522453 q = query
523- result = self.paged_query(q, page=page, sort=sort, columns=columns, **kw)
454+ result = TM.Ticket.paged_search(q, page=page, sort=sort, columns=columns, **kw)
524455 response.headers['Content-Type'] = ''
525456 response.content_type = 'application/xml'
526457 d = dict(title='Ticket search results', link=h.absurl(c.app.url), description='You searched for %s' % q, language=u'en')
@@ -623,7 +554,7 @@ class RootController(BaseController):
623554 sort=validators.UnicodeString(if_empty='ticket_num_i asc')))
624555 def edit(self, q=None, limit=None, page=None, sort=None, **kw):
625556 require_access(c.app, 'update')
626- result = self.paged_query(q, sort=sort, limit=limit, page=page, **kw)
557+ result = TM.Ticket.paged_search(q, sort=sort, limit=limit, page=page, **kw)
627558 # if c.app.globals.milestone_names is None:
628559 # c.app.globals.milestone_names = ''
629560 result['globals'] = c.app.globals
@@ -1386,6 +1317,15 @@ class RootRestController(BaseController):
13861317 log.exception(e)
13871318 return dict(status=False, errors=[str(e)])
13881319
1320+ @expose('json:')
1321+ def search(self, q=None, limit=100, page=0, sort=None, **kw):
1322+ results = TM.Ticket.paged_search(q, limit, page, sort)
1323+ results['tickets'] = [dict(ticket_num=t.ticket_num, summary=t.summary)
1324+ for t in results['tickets']]
1325+ results.pop('sortable_custom_fields', None)
1326+ results.pop('columns', None)
1327+ return results
1328+
13891329 @expose()
13901330 def _lookup(self, ticket_num, *remainder):
13911331 return TicketRestController(ticket_num), remainder