Merge lp:~wallyworld/launchpad/expose-bug-infotype-search-66206 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15600
Proposed branch: lp:~wallyworld/launchpad/expose-bug-infotype-search-66206
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/proprietary-information-type-933782
Diff against target: 389 lines (+151/-21)
12 files modified
lib/lp/bugs/browser/bugtask.py (+9/-1)
lib/lp/bugs/browser/tests/bugtask-search-views.txt (+36/-0)
lib/lp/bugs/interfaces/bugtarget.py (+2/-1)
lib/lp/bugs/interfaces/bugtask.py (+20/-7)
lib/lp/bugs/model/bugtarget.py (+2/-1)
lib/lp/bugs/model/bugtasksearch.py (+5/-2)
lib/lp/bugs/templates/bugtask-macros-tableview.pt (+17/-1)
lib/lp/bugs/tests/test_bugtask_search.py (+2/-2)
lib/lp/bugs/tests/test_searchtasks_webservice.py (+34/-0)
lib/lp/code/model/branch.py (+1/-1)
lib/lp/registry/services/tests/test_sharingservice.py (+21/-3)
lib/lp/registry/vocabularies.py (+2/-2)
To merge this branch: bzr merge lp:~wallyworld/launchpad/expose-bug-infotype-search-66206
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+114147@code.launchpad.net

Commit message

Update bugtask search UI and webservice API to allow searching bugtasks using information type to filter.

Description of the change

== Implementation ==

This branch allows to advanced bugtask search form and webservice api to be used to search for bugtasks filtering on information type. On the ui, like for Importance and Status, the Information Type checkboxes are all unclicked by default (meaning no filtering). The Proprietary option appears as necessary for projects with commercial subscriptions (if feature flag allows).

There was some whitespace to the right of the status and importance checkboxes on the advanaced bugtask search form so I added the information type checkboxes there. It seems to work well even on narrow screens.

The search backend uses the work done in the previous branch. I made some tweaks to improve the usability. eg the caller can just pass in an iterable of info types rather than having to construct an any().

== Demo ==

See screenshot:
http://people.canonical.com/~ianb/bugsearch-infotype.png

== Tests ==

I enhanced bugtask-search-views.txt to test the search view.
I added a new test case to test the search over the webservice: TestSearchByInformationType

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/browser/tests/bugtask-search-views.txt
  lib/lp/bugs/interfaces/bugtarget.py
  lib/lp/bugs/interfaces/bugtask.py
  lib/lp/bugs/model/bugtarget.py
  lib/lp/bugs/model/bugtasksearch.py
  lib/lp/bugs/templates/bugtask-macros-tableview.pt
  lib/lp/bugs/tests/test_bugtask_search.py
  lib/lp/bugs/tests/test_searchtasks_webservice.py
  lib/lp/code/model/branch.py
  lib/lp/registry/vocabularies.py

./lib/lp/bugs/browser/tests/bugtask-search-views.txt
       1: narrative uses a moin header.
     207: narrative uses a moin header.
     222: source exceeds 78 characters.
     286: narrative uses a moin header.
     326: source exceeds 78 characters.
     353: narrative uses a moin header.
     377: source exceeds 78 characters.
     380: source exceeds 78 characters.
     389: narrative uses a moin header.
     451: want exceeds 78 characters.
./lib/lp/bugs/interfaces/bugtask.py
     930: E302 expected 2 blank lines, found 1

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you very much.

Can you fix the link in ./lib/lp/bugs/interfaces/bugtask.py?

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

Sorry, I'm not sure I follow what you need fixed?

On 10/07/12 22:45, Curtis Hovey wrote:
> Review: Approve code
>
> Thank you very much.
>
> Can you fix the link in ./lib/lp/bugs/interfaces/bugtask.py?
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtask.py'
2--- lib/lp/bugs/browser/bugtask.py 2012-07-10 15:39:46 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2012-07-11 01:24:24 +0000
4@@ -241,7 +241,10 @@
5 from lp.registry.interfaces.projectgroup import IProjectGroup
6 from lp.registry.interfaces.sourcepackage import ISourcePackage
7 from lp.registry.model.personroles import PersonRoles
8-from lp.registry.vocabularies import MilestoneVocabulary
9+from lp.registry.vocabularies import (
10+ InformationTypeVocabulary,
11+ MilestoneVocabulary,
12+ )
13 from lp.services.config import config
14 from lp.services.features import getFeatureFlag
15 from lp.services.feeds.browser import (
16@@ -3103,6 +3106,11 @@
17 """Return data used to render the Importance checkboxes."""
18 return self.getWidgetValues(vocabulary=BugTaskImportance)
19
20+ def getInformationTypeWidgetValues(self):
21+ """Return data used to render the Information Type checkboxes."""
22+ return self.getWidgetValues(
23+ vocabulary=InformationTypeVocabulary(self.context))
24+
25 def getMilestoneWidgetValues(self):
26 """Return data used to render the milestone checkboxes."""
27 return self.getWidgetValues("Milestone")
28
29=== modified file 'lib/lp/bugs/browser/tests/bugtask-search-views.txt'
30--- lib/lp/bugs/browser/tests/bugtask-search-views.txt 2011-12-30 06:14:56 +0000
31+++ lib/lp/bugs/browser/tests/bugtask-search-views.txt 2012-07-11 01:24:24 +0000
32@@ -350,6 +350,42 @@
33 Mozilla Firefox 1.0
34
35
36+== Searching by information type ==
37+
38+The advanced form allows us to query for bugs matching specific
39+information types.
40+
41+First we'll change the information type of one of the bugtasks (we are still
42+logged in from earlier):
43+
44+ >>> from lp.registry.enums import InformationType
45+ >>> previous_information_type = open_bugtasks[0].bug.information_type
46+ >>> open_bugtasks[0].bug.transitionToInformationType(
47+ ... InformationType.USERDATA, getUtility(ILaunchBag).user)
48+ True
49+ >>> flush_database_updates()
50+
51+Submit the search:
52+
53+ >>> form_values = {
54+ ... 'search': 'Search bugs in Firefox',
55+ ... 'advanced': 1,
56+ ... 'field.information_type': 'USERDATA',
57+ ... 'field.searchtext': '',
58+ ... 'field.orderby': '-importance'}
59+
60+ >>> mozilla_search_listingview = create_view(mozilla, '+bugs', form_values)
61+ >>> userdata_bugtasks = list(mozilla_search_listingview.search().batch)
62+ >>> for bugtask in userdata_bugtasks:
63+ ... print bugtask.bug.id, bugtask.product.name, bugtask.bug.information_type.name
64+ 15 thunderbird USERDATA
65+
66+ >>> open_bugtasks[0].bug.transitionToInformationType(
67+ ... previous_information_type, getUtility(ILaunchBag).user)
68+ True
69+ >>> flush_database_updates()
70+
71+
72 == Constructing search filter urls ==
73
74 There is a helper method, get_buglisting_search_filter_url(), which can
75
76=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
77--- lib/lp/bugs/interfaces/bugtarget.py 2012-06-14 21:50:59 +0000
78+++ lib/lp/bugs/interfaces/bugtarget.py 2012-07-11 01:24:24 +0000
79@@ -70,6 +70,7 @@
80 "search_text": copy_field(IBugTaskSearch['searchtext']),
81 "status": copy_field(IBugTaskSearch['status']),
82 "importance": copy_field(IBugTaskSearch['importance']),
83+ "information_type": copy_field(IBugTaskSearch['information_type']),
84 "assignee": Reference(schema=Interface),
85 "bug_reporter": Reference(schema=Interface),
86 "bug_supervisor": Reference(schema=Interface),
87@@ -258,7 +259,7 @@
88 hardware_is_linked_to_bug=False, linked_branches=None,
89 linked_blueprints=None, structural_subscriber=None,
90 modified_since=None, created_since=None,
91- created_before=None):
92+ created_before=None, information_type=None):
93 """Search the IBugTasks reported on this entity.
94
95 :search_params: a BugTaskSearchParams object
96
97=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
98--- lib/lp/bugs/interfaces/bugtask.py 2012-07-11 01:24:24 +0000
99+++ lib/lp/bugs/interfaces/bugtask.py 2012-07-11 01:24:24 +0000
100@@ -924,6 +924,10 @@
101 ])
102
103
104+# Avoid circular imports
105+from lp.registry.enums import InformationType
106+
107+
108 class IBugTaskSearchBase(Interface):
109 """The basic search controls."""
110 searchtext = TextLine(title=_("Bug ID or search text."), required=False)
111@@ -943,6 +947,14 @@
112 'or list of importances.'),
113 value_type=IBugTask['importance'],
114 required=False)
115+ information_type = List(
116+ title=_('Information Type'),
117+ description=_('Show only bugs with the given information type '
118+ 'or list of information types.'),
119+ value_type=Choice(
120+ title=_('Information Type'),
121+ vocabulary=InformationType),
122+ required=False)
123 assignee = Choice(
124 title=_('Assignee'),
125 description=_('Person entity assigned for this bug.'),
126@@ -1180,7 +1192,7 @@
127 created_since=None, exclude_conjoined_tasks=False, cve=None,
128 upstream_target=None, milestone_dateexpected_before=None,
129 milestone_dateexpected_after=None, created_before=None,
130- information_types=None):
131+ information_type=None):
132
133 self.bug = bug
134 self.searchtext = searchtext
135@@ -1234,12 +1246,12 @@
136 self.upstream_target = upstream_target
137 self.milestone_dateexpected_before = milestone_dateexpected_before
138 self.milestone_dateexpected_after = milestone_dateexpected_after
139- if isinstance(information_types, collections.Iterable):
140- self.information_types = information_types
141- elif information_types:
142- self.information_types = (information_types,)
143+ if isinstance(information_type, collections.Iterable):
144+ self.information_type = set(information_type)
145+ elif information_type:
146+ self.information_type = set((information_type,))
147 else:
148- self.information_types = None
149+ self.information_type = None
150
151 def setProduct(self, product):
152 """Set the upstream context on which to filter the search."""
153@@ -1385,7 +1397,7 @@
154 hardware_is_linked_to_bug=False, linked_branches=None,
155 linked_blueprints=None, structural_subscriber=None,
156 modified_since=None, created_since=None,
157- created_before=None):
158+ created_before=None, information_type=None):
159 """Create and return a new instance using the parameter list."""
160 search_params = cls(user=user, orderby=order_by)
161
162@@ -1457,6 +1469,7 @@
163 search_params.modified_since = modified_since
164 search_params.created_since = created_since
165 search_params.created_before = created_before
166+ search_params.information_type = information_type
167
168 return search_params
169
170
171=== modified file 'lib/lp/bugs/model/bugtarget.py'
172--- lib/lp/bugs/model/bugtarget.py 2012-06-14 21:50:59 +0000
173+++ lib/lp/bugs/model/bugtarget.py 2012-07-11 01:24:24 +0000
174@@ -81,7 +81,8 @@
175 hardware_owner_is_subscribed_to_bug=False,
176 hardware_is_linked_to_bug=False, linked_branches=None,
177 linked_blueprints=None, modified_since=None,
178- created_since=None, created_before=None):
179+ created_since=None, created_before=None,
180+ information_type=None):
181 """See `IHasBugs`."""
182 if status is None:
183 # If no statuses are supplied, default to the
184
185=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
186--- lib/lp/bugs/model/bugtasksearch.py 2012-07-11 01:24:24 +0000
187+++ lib/lp/bugs/model/bugtasksearch.py 2012-07-11 01:24:24 +0000
188@@ -182,6 +182,8 @@
189
190 def search_value_to_storm_where_condition(comp, search_value):
191 """Convert a search value to a Storm WHERE condition."""
192+ if zope_isinstance(search_value, (set, list, tuple)):
193+ search_value = any(*search_value)
194 if zope_isinstance(search_value, any):
195 # When an any() clause is provided, the argument value
196 # is a list of acceptable filter values.
197@@ -734,9 +736,10 @@
198 extra_clauses.append(
199 BugTaskFlat.datecreated < params.created_before)
200
201- if params.information_types:
202+ if params.information_type:
203 extra_clauses.append(
204- BugTaskFlat.information_type.is_in(params.information_types))
205+ search_value_to_storm_where_condition(
206+ BugTaskFlat.information_type, params.information_type))
207
208 query = And(extra_clauses)
209
210
211=== modified file 'lib/lp/bugs/templates/bugtask-macros-tableview.pt'
212--- lib/lp/bugs/templates/bugtask-macros-tableview.pt 2012-06-27 19:23:40 +0000
213+++ lib/lp/bugs/templates/bugtask-macros-tableview.pt 2012-07-11 01:24:24 +0000
214@@ -273,7 +273,7 @@
215 </tal:checkbox>
216 </div>
217 </td>
218- <td width="30%">
219+ <td width="25%">
220 <div><label>Importance:</label></div>
221 <div tal:repeat="widget_value view/getImportanceWidgetValues">
222 <tal:checkbox
223@@ -289,6 +289,22 @@
224 </tal:checkbox>
225 </div>
226 </td>
227+ <td width="35%">
228+ <div><label>Information Type:</label></div>
229+ <div tal:repeat="widget_value view/getInformationTypeWidgetValues">
230+ <tal:checkbox
231+ define="widget_id string:information_type.${widget_value/title}">
232+ <input name="field.information_type:list"
233+ type="checkbox"
234+ tal:attributes="value widget_value/value;
235+ checked widget_value/checked;
236+ id widget_id"/>
237+ <label style="font-weight: normal"
238+ tal:content="widget_value/title"
239+ tal:attributes="for widget_id">Public</label>
240+ </tal:checkbox>
241+ </div>
242+ </td>
243 </tr>
244 </table>
245 </fieldset>
246
247=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
248--- lib/lp/bugs/tests/test_bugtask_search.py 2012-07-11 01:24:24 +0000
249+++ lib/lp/bugs/tests/test_bugtask_search.py 2012-07-11 01:24:24 +0000
250@@ -417,11 +417,11 @@
251 InformationType.EMBARGOEDSECURITY, self.owner)
252 params = self.getBugTaskSearchParams(
253 user=self.owner,
254- information_types=InformationType.EMBARGOEDSECURITY)
255+ information_type=InformationType.EMBARGOEDSECURITY)
256 self.assertSearchFinds(params, [self.bugtasks[2]])
257 params = self.getBugTaskSearchParams(
258 user=self.owner,
259- information_types=InformationType.UNEMBARGOEDSECURITY)
260+ information_type=InformationType.UNEMBARGOEDSECURITY)
261 self.assertSearchFinds(params, [])
262
263 def test_omit_duplicate_bugs(self):
264
265=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
266--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-01-01 02:58:52 +0000
267+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-07-11 01:24:24 +0000
268@@ -5,6 +5,7 @@
269
270 __metaclass__ = type
271
272+from lp.registry.enums import InformationType
273 from lp.testing import (
274 person_logged_in,
275 TestCaseWithFactory,
276@@ -81,3 +82,36 @@
277 # validation is performed for the linked_blueprints parameter, and
278 # thus no error is returned when we pass rubbish.
279 self.search("beta", linked_blueprints="Teabags!")
280+
281+
282+class TestSearchByInformationType(TestCaseWithFactory):
283+ """Tests for the information_type parameter."""
284+
285+ layer = DatabaseFunctionalLayer
286+
287+ def setUp(self):
288+ super(TestSearchByInformationType, self).setUp()
289+ self.owner = self.factory.makePerson()
290+ with person_logged_in(self.owner):
291+ self.product = self.factory.makeProduct()
292+ self.bug = self.factory.makeBug(
293+ product=self.product,
294+ information_type=InformationType.EMBARGOEDSECURITY)
295+ self.webservice = LaunchpadWebServiceCaller(
296+ 'launchpad-library', 'salgado-change-anything')
297+
298+ def search(self, api_version, **kwargs):
299+ return self.webservice.named_get(
300+ '/%s' % self.product.name, 'searchTasks',
301+ api_version=api_version, **kwargs).jsonBody()
302+
303+ def test_search_returns_results(self):
304+ # A matching search returns results.
305+ response = self.search(
306+ "devel", information_type="Embargoed Security")
307+ self.assertEqual(response['total_size'], 1)
308+
309+ def test_search_returns_no_results(self):
310+ # A non-matching search returns no results.
311+ response = self.search("devel", information_type="User Data")
312+ self.assertEqual(response['total_size'], 0)
313
314=== modified file 'lib/lp/code/model/branch.py'
315--- lib/lp/code/model/branch.py 2012-07-11 01:24:24 +0000
316+++ lib/lp/code/model/branch.py 2012-07-11 01:24:24 +0000
317@@ -1318,7 +1318,7 @@
318 # Branches linked to private bugs can be private.
319 params = BugTaskSearchParams(
320 user=user, linked_branches=self.id,
321- information_types=PRIVATE_INFORMATION_TYPES)
322+ information_type=PRIVATE_INFORMATION_TYPES)
323 bug_ids = getUtility(IBugTaskSet).searchBugIds(params)
324 return bug_ids.count() > 0
325
326
327=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
328--- lib/lp/registry/services/tests/test_sharingservice.py 2012-07-11 01:24:24 +0000
329+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-07-11 01:24:24 +0000
330@@ -402,6 +402,25 @@
331 login_person(self.factory.makePerson())
332 self._assert_getPillarShareesUnauthorized(product)
333
334+ def _assert_sharee_data(self, expected, actual):
335+ # Assert that the actual and expected sharee data is equal.
336+ # Sharee data is a list of (sharee, permissions, info_types) tuples.
337+ expected_list = list(expected)
338+ actual_list = list(actual)
339+ self.assertEqual(len(expected_list), len(list(actual_list)))
340+
341+ expected_sharee_map = {}
342+ for data in expected_list:
343+ expected_sharee_map[data[0]] = data[1:]
344+ actual_sharee_map = {}
345+ for data in actual_list:
346+ actual_sharee_map[data[0]] = data[1:]
347+
348+ for sharee, expected_permissions, expected_info_types in expected:
349+ actual_permissions, actual_info_types = actual_sharee_map[sharee]
350+ self.assertContentEqual(expected_permissions, actual_permissions)
351+ self.assertContentEqual(expected_info_types, actual_info_types)
352+
353 def _assert_sharePillarInformation(self, pillar, pillar_type=None):
354 """sharePillarInformations works and returns the expected data."""
355 sharee = self.factory.makePerson()
356@@ -464,7 +483,7 @@
357 expected_sharee_data = self._makeShareeData(
358 sharee, expected_permissions,
359 [InformationType.EMBARGOEDSECURITY, InformationType.USERDATA])
360- self.assertEqual(expected_sharee_data, sharee_data)
361+ self.assertContentEqual(expected_sharee_data, sharee_data)
362 # Check that getPillarSharees returns what we expect.
363 if pillar_type == 'product':
364 expected_sharee_grants = [
365@@ -618,8 +637,7 @@
366 policy, SharingPermission.ALL) for policy in access_policies])
367 owner_data = (pillar.owner, policy_permissions, [])
368 expected_data.append(owner_data)
369-
370- self.assertContentEqual(
371+ self._assert_sharee_data(
372 expected_data, self.service.getPillarSharees(pillar))
373
374 def test_deleteProductShareeAll(self):
375
376=== modified file 'lib/lp/registry/vocabularies.py'
377--- lib/lp/registry/vocabularies.py 2012-07-11 01:24:24 +0000
378+++ lib/lp/registry/vocabularies.py 2012-07-11 01:24:24 +0000
379@@ -2276,8 +2276,8 @@
380 IProduct.providedBy(subscription_context) and
381 subscription_context.has_current_commercial_subscription)
382 already_proprietary = (
383- safe_hasattr(context, 'information_type')
384- and context.information_type == InformationType.PROPRIETARY)
385+ safe_hasattr(context, 'information_type') and
386+ context.information_type == InformationType.PROPRIETARY)
387 if has_commercial_subscription or already_proprietary:
388 types.append(InformationType.PROPRIETARY)
389 # Disallow public items for projects with private bugs.