allura
Revisión | ff74de6ff9840e44229fa8dbf8272e63189eb624 (tree) |
---|---|
Tiempo | 2012-07-11 12:45:16 |
Autor | Dave Brondsema <dbrondsema@geek...> |
Commiter | Dave Brondsema |
[#4272] add rest API for ticket searches; refactor paged_search to better name & location
@@ -578,7 +578,11 @@ class Ticket(VersionedArtifact, ActivityObject): | ||
578 | 578 | |
579 | 579 | @classmethod |
580 | 580 | 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 | + """ | |
582 | 586 | limit, page, start = g.handle_paging(limit, page, default=25) |
583 | 587 | q = cls.query.find(dict(query, app_config_id=c.app.config._id)) |
584 | 588 | q = q.sort('ticket_num') |
@@ -616,6 +620,78 @@ class Ticket(VersionedArtifact, ActivityObject): | ||
616 | 620 | count=count, q=json.dumps(query), limit=limit, page=page, sort=sort, |
617 | 621 | **kw) |
618 | 622 | |
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 | + | |
619 | 695 | class TicketAttachment(BaseAttachment): |
620 | 696 | thumbnail_size = (100, 100) |
621 | 697 | ArtifactType=Ticket |
@@ -3,10 +3,14 @@ pylons.c = pylons.tmpl_context | ||
3 | 3 | pylons.g = pylons.app_globals |
4 | 4 | from pylons import c |
5 | 5 | |
6 | +from datadiff.tools import assert_equal | |
7 | +from mock import patch | |
8 | + | |
6 | 9 | from allura.lib import helpers as h |
7 | 10 | from allura.tests import decorators as td |
8 | 11 | from alluratest.controller import TestRestApiBase |
9 | 12 | |
13 | +from forgetracker import model as TM | |
10 | 14 | |
11 | 15 | class TestTrackerApiBase(TestRestApiBase): |
12 | 16 |
@@ -113,3 +117,36 @@ class TestRestDiscussion(TestTrackerApiBase): | ||
113 | 117 | assert reply.json['post']['text'] == 'This is a reply', reply.json |
114 | 118 | thread = self.api_get('/rest/p/test/bugs/_discuss/thread/%s/' % discussion['threads'][0]['_id']) |
115 | 119 | 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 | + }) |
@@ -49,7 +49,7 @@ def solr_search_returning_colors_are_wrong_ticket(): | ||
49 | 49 | matches = Mock() |
50 | 50 | matches.docs = [dict(ticket_num_i=ticket.ticket_num)] |
51 | 51 | 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) | |
53 | 53 | |
54 | 54 | def mongo_search_returning_colors_are_wrong_ticket(): |
55 | 55 | ticket = create_colors_are_wrong_ticket() |
@@ -82,4 +82,3 @@ def create_ticket(summary, custom_fields): | ||
82 | 82 | custom_fields=custom_fields) |
83 | 83 | session(ticket).flush() |
84 | 84 | return ticket |
85 | - |
@@ -331,75 +331,6 @@ class RootController(BaseController): | ||
331 | 331 | bin_counts.append(dict(label=label, count=count)) |
332 | 332 | return dict(bin_counts=bin_counts) |
333 | 333 | |
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 | - | |
403 | 334 | @with_trailing_slash |
404 | 335 | @h.vardec |
405 | 336 | @expose('jinja:forgetracker:templates/tracker/index.html') |
@@ -506,7 +437,7 @@ class RootController(BaseController): | ||
506 | 437 | bin = TM.Bin.query.find(dict(app_config_id=c.app.config._id,terms=q)).first() |
507 | 438 | if project: |
508 | 439 | 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) | |
510 | 441 | result['allow_edit'] = has_access(c.app, 'update')() |
511 | 442 | result['bin'] = bin |
512 | 443 | result['help_msg'] = c.app.config.options.get('TicketHelpSearch') |
@@ -520,7 +451,7 @@ class RootController(BaseController): | ||
520 | 451 | def search_feed(self, q=None, query=None, project=None, columns=None, page=0, sort=None, **kw): |
521 | 452 | if query and not q: |
522 | 453 | 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) | |
524 | 455 | response.headers['Content-Type'] = '' |
525 | 456 | response.content_type = 'application/xml' |
526 | 457 | 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): | ||
623 | 554 | sort=validators.UnicodeString(if_empty='ticket_num_i asc'))) |
624 | 555 | def edit(self, q=None, limit=None, page=None, sort=None, **kw): |
625 | 556 | 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) | |
627 | 558 | # if c.app.globals.milestone_names is None: |
628 | 559 | # c.app.globals.milestone_names = '' |
629 | 560 | result['globals'] = c.app.globals |
@@ -1386,6 +1317,15 @@ class RootRestController(BaseController): | ||
1386 | 1317 | log.exception(e) |
1387 | 1318 | return dict(status=False, errors=[str(e)]) |
1388 | 1319 | |
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 | + | |
1389 | 1329 | @expose() |
1390 | 1330 | def _lookup(self, ticket_num, *remainder): |
1391 | 1331 | return TicketRestController(ticket_num), remainder |