launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #04711
[Merge] lp:~wallyworld/launchpad/filter-person-picker-3938 into lp:launchpad
Ian Booth has proposed merging lp:~wallyworld/launchpad/filter-person-picker-3938 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #3938 in Launchpad itself: "Not possible to search only for teams in the picker"
https://bugs.launchpad.net/launchpad/+bug/3938
For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/filter-person-picker-3938/+merge/72420
Apply the recently added vocab filtering infrastructure to the person picker to allow filtering on person/team.
== Implementation ==
The core changes are straightforward - create the new filters and implement the supportedFilters() method on the relevant vocabs. Other changes were also required:
- the vocab search() method signature needed to be modified to take the vocab_filter parameter; hence also changes to FilteredVocabularyBase
- add filtering support to InlineEditorPickerWidget
- add filtering support to bugtask assignee field since this widget is constructed manaually
== Demo / QA ==
Use the assignee picker to change a bug task assignee
Use the approver picker to change a bugtask approver
Change a branch reviewer
== Tests ==
Add tests for the core filtering behaviour on the person vocab, plus widget setup.
bin/test -vvc -t test_vocabulary -t test_popup -t test_inlineeditpickerwidget -t test_person_vocabularies -t vocabularies
bin/test --layer=YUI
== Lint ==
Linting changed files:
lib/canonical/launchpad/vocabularies/dbobjects.py
lib/canonical/launchpad/webapp/vocabulary.py
lib/canonical/launchpad/webapp/tests/test_vocabulary.py
lib/lp/app/browser/lazrjs.py
lib/lp/app/browser/tests/test_inlineeditpickerwidget.py
lib/lp/app/javascript/picker/picker_patcher.js
lib/lp/app/javascript/picker/tests/test_picker_patcher.js
lib/lp/app/widgets/popup.py
lib/lp/app/widgets/tests/test_popup.py
lib/lp/blueprints/vocabularies/specificationdependency.py
lib/lp/bugs/browser/bugtask.py
lib/lp/bugs/javascript/bugtask_index.js
lib/lp/registry/vocabularies.py
lib/lp/registry/vocabularies.zcml
lib/lp/registry/tests/test_person_vocabularies.py
--
https://code.launchpad.net/~wallyworld/launchpad/filter-person-picker-3938/+merge/72420
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/filter-person-picker-3938 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/vocabularies/dbobjects.py'
--- lib/canonical/launchpad/vocabularies/dbobjects.py 2011-08-10 10:11:16 +0000
+++ lib/canonical/launchpad/vocabularies/dbobjects.py 2011-08-22 13:16:25 +0000
@@ -162,7 +162,7 @@
raise LookupError(token)
return self.toTerm(result)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Search for web bug trackers."""
query = ensure_unicode(query).lower()
results = IStore(self._table).find(
@@ -179,7 +179,7 @@
def searchForTerms(self, query=None, vocab_filter=None):
"""See `IHugeVocabulary`."""
- results = self.search(query)
+ results = self.search(query, vocab_filter)
return CountableIterator(results.count(), results, self.toTerm)
@@ -436,7 +436,7 @@
else:
return self.toTerm(obj)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Return a resultset of archives.
This is a helper required by `SQLObjectVocabularyBase.searchForTerms`.
=== modified file 'lib/canonical/launchpad/webapp/tests/test_vocabulary.py'
--- lib/canonical/launchpad/webapp/tests/test_vocabulary.py 2011-08-20 10:53:02 +0000
+++ lib/canonical/launchpad/webapp/tests/test_vocabulary.py 2011-08-22 13:16:25 +0000
@@ -18,6 +18,11 @@
class TestVocabulary(FilteredVocabularyBase):
implements(IHugeVocabulary)
+ def search(self, query=None, vocab_filter=None):
+ assert(isinstance(vocab_filter, VocabularyFilter))
+ assert(vocab_filter.name == "ALL")
+ assert(vocab_filter.title == "All")
+
def searchForTerms(self, query=None, vocab_filter=None):
assert(isinstance(vocab_filter, VocabularyFilter))
assert(vocab_filter.name == "ALL")
@@ -49,3 +54,22 @@
vocab = TestVocabulary()
self.assertRaises(
ValueError, vocab.searchForTerms, vocab_filter="invalid")
+
+ def test_search_filter_parameter_as_string(self):
+ # If the vocab filter parameter is passed in as a string (name), it is
+ # correctly transformed to a VocabularyFilter instance.
+ vocab = TestVocabulary()
+ vocab.search(vocab_filter="ALL")
+
+ def test_search_filter_parameter_as_filter(self):
+ # If the vocab filter parameter is passed in as a filter instance, it
+ # is used as is.
+ vocab = TestVocabulary()
+ vocab.search(vocab_filter=FilteredVocabularyBase.ALL_FILTER)
+
+ def test_search_invalid_filter_parameter(self):
+ # If the vocab filter parameter is passed in as a string (name), and
+ # the string is not a valid filter name, an exception is raised.
+ vocab = TestVocabulary()
+ self.assertRaises(
+ ValueError, vocab.search, vocab_filter="invalid")
=== modified file 'lib/canonical/launchpad/webapp/vocabulary.py'
--- lib/canonical/launchpad/webapp/vocabulary.py 2011-08-20 10:53:02 +0000
+++ lib/canonical/launchpad/webapp/vocabulary.py 2011-08-22 13:16:25 +0000
@@ -256,8 +256,10 @@
def __getattribute__(self, name):
func = object.__getattribute__(self, name)
if (safe_hasattr(func, '__call__')
- and func.__name__ == 'searchForTerms'):
- def searchForTerms(
+ and (
+ func.__name__ == 'searchForTerms'
+ or func.__name__ == 'search')):
+ def do_search(
query=None, vocab_filter=None, *args, **kwargs):
if isinstance(vocab_filter, basestring):
for filter in self.supportedFilters():
@@ -268,7 +270,7 @@
raise ValueError(
"Invalid vocab filter value: %s" % vocab_filter)
return func(query, vocab_filter, *args, **kwargs)
- return searchForTerms
+ return do_search
else:
return func
@@ -309,7 +311,7 @@
results = self.search(query)
return CountableIterator(results.count(), results, self.toTerm)
- def search(self):
+ def search(self, query, vocab_filter=None):
# This default implementation of searchForTerms glues together
# the legacy API of search() with the toTerm method. If you
# don't reimplement searchForTerms you will need to at least
@@ -422,7 +424,7 @@
raise LookupError(token)
return self.toTerm(objs[0])
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Return terms where query is a subtring of the name."""
if query:
clause = CONTAINSSTRING(self._table.q.name, ensure_unicode(query))
@@ -449,7 +451,7 @@
if self.displayname is None:
self.displayname = 'Select %s' % self.__class__.__name__
- def search(self, query):
+ def search(self, query, vocab_filter=None):
# XXX kiko 2007-01-17: this is a transitional shim; we're going to
# get rid of search() altogether, but for now we've only done it for
# the NamedSQLObjectHugeVocabulary.
=== modified file 'lib/lp/app/browser/lazrjs.py'
--- lib/lp/app/browser/lazrjs.py 2011-07-27 05:05:46 +0000
+++ lib/lp/app/browser/lazrjs.py 2011-08-22 13:16:25 +0000
@@ -306,7 +306,8 @@
selected_value=self.selected_value,
selected_value_metadata=self.selected_value_metadata,
null_display_value=self.null_display_value,
- show_search_box=self.show_search_box)
+ show_search_box=self.show_search_box,
+ vocabulary_filters=self.vocabulary_filters)
@property
def json_config(self):
@@ -318,6 +319,27 @@
return registry.get(
IVocabulary, self.exported_field.vocabularyName)
+ @cachedproperty
+ def vocabulary_filters(self):
+ # Only IHugeVocabulary's have filters.
+ if not IHugeVocabulary.providedBy(self.vocabulary):
+ return []
+ supported_filters = self.vocabulary.supportedFilters()
+ # If we have no filters or just the ALL filter, then no filtering
+ # support is required.
+ filters = []
+ if (len(supported_filters) == 0 or
+ (len(supported_filters) == 1
+ and supported_filters[0].name == 'ALL')):
+ return filters
+ for filter in supported_filters:
+ filters.append({
+ 'name': filter.name,
+ 'title': filter.title,
+ 'description': filter.description,
+ })
+ return filters
+
@property
def show_search_box(self):
return IHugeVocabulary.providedBy(self.vocabulary)
=== modified file 'lib/lp/app/browser/tests/test_inlineeditpickerwidget.py'
--- lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2011-07-27 05:05:46 +0000
+++ lib/lp/app/browser/tests/test_inlineeditpickerwidget.py 2011-08-22 13:16:25 +0000
@@ -39,6 +39,25 @@
widget = self.getWidget(vocabulary='ValidPersonOrTeam')
self.assertTrue(widget.config['show_search_box'])
+ def test_vocabulary_filters(self):
+ # Make sure that when given a vocabulary which supports vocab filters,
+ # the vocab filters are include in the widget config.
+ widget = self.getWidget(vocabulary='ValidPersonOrTeam')
+ self.assertEquals([
+ {'name': 'ALL',
+ 'title': 'All',
+ 'description': 'Display all search results'},
+ {'name': 'PERSON',
+ 'title': 'Person',
+ 'description':
+ 'Display search results for people only'},
+ {'name': 'TEAM',
+ 'title': 'Team',
+ 'description':
+ 'Display search results for teams only'}
+ ],
+ widget.config['vocabulary_filters'])
+
def test_normal_vocabulary_is_not_searchable(self):
# Make sure that when given a field for a normal vocabulary, the
# picker is set to show the search box.
=== modified file 'lib/lp/app/javascript/picker/picker_patcher.js'
--- lib/lp/app/javascript/picker/picker_patcher.js 2011-08-19 03:00:28 +0000
+++ lib/lp/app/javascript/picker/picker_patcher.js 2011-08-22 13:16:25 +0000
@@ -37,6 +37,7 @@
var null_display_value = 'None';
var show_search_box = true;
+ var vocabulary_filters;
resource_uri = Y.lp.client.normalize_uri(resource_uri);
@@ -47,6 +48,7 @@
if (config.show_search_box !== undefined) {
show_search_box = config.show_search_box;
}
+ vocabulary_filters = config.vocabulary_filters;
}
var content_box = Y.one('#' + content_box_id);
@@ -97,7 +99,8 @@
};
config.save = save;
- var picker = namespace.create(vocabulary, config);
+ var picker = namespace.create(
+ vocabulary, config, undefined, vocabulary_filters);
picker._resource_uri = resource_uri;
// If we are to pre-load the vocab, we need a spinner.
=== modified file 'lib/lp/app/javascript/picker/tests/test_picker_patcher.js'
--- lib/lp/app/javascript/picker/tests/test_picker_patcher.js 2011-08-19 03:00:28 +0000
+++ lib/lp/app/javascript/picker/tests/test_picker_patcher.js 2011-08-22 13:16:25 +0000
@@ -271,7 +271,7 @@
cleanup_widget(this.picker);
},
- create_picker: function(show_search_box) {
+ create_picker: function(show_search_box, extra_config) {
var config = {
"step_title": "Choose someone",
"header": "Pick Someone",
@@ -280,6 +280,9 @@
if (show_search_box !== undefined) {
config.show_search_box = show_search_box;
}
+ if (extra_config !== undefined) {
+ config = Y.merge(extra_config, config);
+ }
this.picker = Y.lp.app.picker.addPickerPatcher(
this.vocabulary,
"foo/bar",
@@ -336,8 +339,15 @@
this.picker.set('min_search_chars', 0);
this.picker.fire('search', '');
Assert.areEqual(null, this.picker.get('error'));
+ },
+
+ test_vocab_filter_config: function () {
+ // The vocab filter config is correctly used to create the picker.
+ var filters = [{name: 'ALL', title: 'All', description: 'All'}];
+ this.create_picker(undefined, {'vocabulary_filters': filters});
+ var filter_options = this.picker.get('filter_options');
+ Assert.areEqual(filters, filter_options);
}
-
}));
suite.add(new Y.Test.Case({
=== modified file 'lib/lp/app/widgets/popup.py'
--- lib/lp/app/widgets/popup.py 2011-08-18 08:00:28 +0000
+++ lib/lp/app/widgets/popup.py 2011-08-22 13:16:25 +0000
@@ -17,6 +17,7 @@
from zope.schema.interfaces import IChoice
from canonical.launchpad.webapp import canonical_url
+from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
from canonical.lazr.utils import safe_hasattr
from lp.app.browser.stringformatter import FormattersAPI
from lp.app.browser.vocabulary import get_person_picker_entry_metadata
@@ -166,6 +167,9 @@
"The %r.%s interface attribute doesn't have its "
"vocabulary specified."
% (choice.context, choice.__name__))
+ # Only IHugeVocabulary's have filters.
+ if not IHugeVocabulary.providedBy(choice.vocabulary):
+ return []
supported_filters = choice.vocabulary.supportedFilters()
# If we have no filters or just the ALL filter, then no filtering
# support is required.
=== modified file 'lib/lp/app/widgets/tests/test_popup.py'
--- lib/lp/app/widgets/tests/test_popup.py 2011-08-18 08:00:28 +0000
+++ lib/lp/app/widgets/tests/test_popup.py 2011-08-22 13:16:25 +0000
@@ -64,7 +64,17 @@
widget_config = simplejson.loads(picker_widget.json_config)
self.assertEqual(
'ValidTeamOwner', picker_widget.vocabulary_name)
- self.assertEqual([], picker_widget.vocabulary_filters)
+ self.assertEqual([
+ {'name': 'ALL',
+ 'title': 'All',
+ 'description': 'Display all search results'},
+ {'name': 'PERSON',
+ 'title': 'Person',
+ 'description': 'Display search results for people only'},
+ {'name': 'TEAM',
+ 'title': 'Team',
+ 'description': 'Display search results for teams only'}
+ ], picker_widget.vocabulary_filters)
self.assertEqual(self.vocabulary.displayname, widget_config['header'])
self.assertEqual(self.vocabulary.step_title,
widget_config['step_title'])
=== modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py'
--- lib/lp/blueprints/vocabularies/specificationdependency.py 2011-03-15 02:49:11 +0000
+++ lib/lp/blueprints/vocabularies/specificationdependency.py 2011-08-22 13:16:25 +0000
@@ -27,7 +27,6 @@
NamedSQLObjectVocabulary,
SQLObjectVocabularyBase,
)
-from lp.blueprints.interfaces.specification import ISpecification
from lp.blueprints.model.specification import (
recursive_blocked_query,
Specification,
@@ -163,7 +162,7 @@
return self.toTerm(spec)
raise LookupError(token)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""See `SQLObjectVocabularyBase.search`.
We find specs where query is in the text of name or title, or matches
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-08-15 22:24:35 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-08-22 13:16:25 +0000
@@ -271,6 +271,7 @@
from lp.services.fields import PersonChoice
from lp.services.propertycache import cachedproperty
+vocabulary_registry = getVocabularyRegistry()
DISPLAY_BUG_STATUS_FOR_PATCHES = {
BugTaskStatus.NEW: True,
@@ -1053,12 +1054,14 @@
return '_'.join(parts)
-def get_assignee_vocabulary(context):
+def get_assignee_vocabulary_info(context):
"""The vocabulary of bug task assignees the current user can set."""
if context.userCanSetAnyAssignee(getUtility(ILaunchBag).user):
- return 'ValidAssignee'
+ vocab_name = 'ValidAssignee'
else:
- return 'AllUserTeamsParticipation'
+ vocab_name = 'AllUserTeamsParticipation'
+ vocab = vocabulary_registry.get(None, vocab_name)
+ return vocab_name, vocab.supportedFilters()
class BugTaskBugWatchMixin:
@@ -1339,10 +1342,10 @@
self.form_fields.get('assignee', False)):
# Make the assignee field editable
self.form_fields = self.form_fields.omit('assignee')
+ vocabulary, ignored = get_assignee_vocabulary_info(self.context)
self.form_fields += formlib.form.Fields(PersonChoice(
__name__='assignee', title=_('Assigned to'), required=False,
- vocabulary=get_assignee_vocabulary(self.context),
- readonly=False))
+ vocabulary=vocabulary, readonly=False))
self.form_fields['assignee'].custom_widget = CustomWidgetFactory(
BugTaskAssigneeWidget)
@@ -2654,7 +2657,6 @@
if vocabulary is None:
assert vocabulary_name is not None, 'No vocabulary specified.'
- vocabulary_registry = getVocabularyRegistry()
vocabulary = vocabulary_registry.get(
self.context, vocabulary_name)
for term in vocabulary:
@@ -3586,7 +3588,21 @@
def js_config(self):
"""Configuration for the JS widgets on the row, JSON-serialized."""
- assignee_vocabulary = get_assignee_vocabulary(self.context)
+ assignee_vocabulary, assignee_vocabulary_filters = (
+ get_assignee_vocabulary_info(self.context))
+ # If we have no filters or just the ALL filter, then no filtering
+ # support is required.
+ filter_details = []
+ if (len(assignee_vocabulary_filters) > 1 or
+ (len(assignee_vocabulary_filters) == 1
+ and assignee_vocabulary_filters[0].name != 'ALL')):
+ for filter in assignee_vocabulary_filters:
+ filter_details.append({
+ 'name': filter.name,
+ 'title': filter.title,
+ 'description': filter.description,
+ })
+
# Display the search field only if the user can set any person
# or team
user = getUtility(ILaunchBag).user
@@ -3603,6 +3619,7 @@
'assignee_is_team': self.context.assignee
and self.context.assignee.is_team,
'assignee_vocabulary': assignee_vocabulary,
+ 'assignee_vocabulary_filters': filter_details,
'hide_assignee_team_selection': hide_assignee_team_selection,
'user_can_unassign': self.context.userCanUnassign(user),
'target_is_product': IProduct.providedBy(self.context.target),
=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
--- lib/lp/bugs/javascript/bugtask_index.js 2011-08-11 22:41:38 +0000
+++ lib/lp/bugs/javascript/bugtask_index.js 2011-08-22 13:16:25 +0000
@@ -850,6 +850,7 @@
"assignee_link",
assignee_content.get('id'),
{"picker_type": "person",
+ "vocabulary_filters": conf.assignee_vocabulary_filters,
"step_title": step_title,
"header": "Change assignee",
"selected_value": conf.assignee_value,
=== modified file 'lib/lp/registry/tests/test_person_vocabularies.py'
--- lib/lp/registry/tests/test_person_vocabularies.py 2011-06-21 05:52:56 +0000
+++ lib/lp/registry/tests/test_person_vocabularies.py 2011-08-22 13:16:25 +0000
@@ -12,6 +12,7 @@
from zope.security.proxy import removeSecurityProxy
from canonical.launchpad.ftests import login_person
+from canonical.launchpad.webapp.vocabulary import FilteredVocabularyBase
from canonical.testing.layers import (
DatabaseFunctionalLayer,
LaunchpadZopelessLayer,
@@ -22,6 +23,7 @@
TeamSubscriptionPolicy,
)
from lp.registry.interfaces.karma import IKarmaCacheManager
+from lp.registry.vocabularies import ValidPersonOrTeamVocabulary
from lp.services.features.testing import FeatureFixture
from lp.testing import (
StormStatementRecorder,
@@ -48,11 +50,12 @@
def getVocabulary(self, context):
return self.vocabulary_registry.get(context, self.vocabulary_name)
- def searchVocabulary(self, context, text):
+ def searchVocabulary(self, context, text, vocab_filter=None):
if Store.of(context) is not None:
Store.of(context).flush()
vocabulary = self.getVocabulary(context)
- return removeSecurityProxy(vocabulary)._doSearch(text)
+ removeSecurityProxy(vocabulary).allow_null_search = True
+ return removeSecurityProxy(vocabulary).search(text, vocab_filter)
class TestValidPersonOrTeamVocabulary(VocabularyTestBase,
@@ -65,6 +68,16 @@
layer = LaunchpadZopelessLayer
vocabulary_name = 'ValidPersonOrTeam'
+ def test_supported_filters(self):
+ # The vocab supports the correct filters.
+ self.assertEqual([
+ FilteredVocabularyBase.ALL_FILTER,
+ ValidPersonOrTeamVocabulary.PERSON_FILTER,
+ ValidPersonOrTeamVocabulary.TEAM_FILTER,
+ ],
+ self.getVocabulary(None).supportedFilters()
+ )
+
def addKarma(self, person, value, product=None, distribution=None):
if product:
kwargs = dict(product_id=product.id)
@@ -138,6 +151,42 @@
self.assertContentEqual(
[person], self.searchVocabulary(person, irc.nickname.lower()))
+ def _person_filter_tests(self, person):
+ results = self.searchVocabulary(None, '', 'PERSON')
+ for personorteam in results:
+ self.assertFalse(personorteam.is_team)
+ results = self.searchVocabulary(None, u'fred', 'PERSON')
+ self.assertEqual([person], list(results))
+
+ def test_person_filter(self):
+ # Test that the person filter only returns people
+ # (with and without feature flag).
+ person = self.factory.makePerson(
+ name="fredperson", email="fredperson@xxxxxxx")
+ self.factory.makeTeam(
+ name="fredteam", email="fredteam@xxxxxxx")
+ with FeatureFixture(PERSON_AFFILIATION_RANK_FLAG):
+ self._person_filter_tests(person)
+ self._person_filter_tests(person)
+
+ def _team_filter_tests(self, team):
+ results = self.searchVocabulary(None, '', 'TEAM')
+ for personorteam in results:
+ self.assertTrue(personorteam.is_team)
+ results = self.searchVocabulary(None, u'fred', 'TEAM')
+ self.assertEqual([team], list(results))
+
+ def test_team_filter(self):
+ # Test that the team filter only returns teams.
+ # (with and without feature flag).
+ self.factory.makePerson(
+ name="fredperson", email="fredperson@xxxxxxx")
+ team = self.factory.makeTeam(
+ name="fredteam", email="fredteam@xxxxxxx")
+ with FeatureFixture(PERSON_AFFILIATION_RANK_FLAG):
+ self._team_filter_tests(team)
+ self._team_filter_tests(team)
+
class TestValidPersonOrTeamPreloading(VocabularyTestBase,
TestCaseWithFactory):
@@ -271,3 +320,27 @@
owned_team = self.factory.makeTeam(owner=context_team)
results = self.searchVocabulary(context_team, owned_team.name)
self.assertNotIn(owned_team, results)
+
+
+class TestValidPersonVocabulary(VocabularyTestBase,
+ TestCaseWithFactory):
+ """Test that the ValidPersonVocabulary behaves as expected."""
+
+ layer = LaunchpadZopelessLayer
+ vocabulary_name = 'ValidPerson'
+
+ def test_supported_filters(self):
+ # The vocab shouldn't support person or team filters.
+ self.assertEqual([], self.getVocabulary(None).supportedFilters())
+
+
+class TestValidTeamVocabulary(VocabularyTestBase,
+ TestCaseWithFactory):
+ """Test that the ValidTeamVocabulary behaves as expected."""
+
+ layer = LaunchpadZopelessLayer
+ vocabulary_name = 'ValidTeam'
+
+ def test_supported_filters(self):
+ # The vocab shouldn't support person or team filters.
+ self.assertEqual([], self.getVocabulary(None).supportedFilters())
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2011-08-20 10:53:02 +0000
+++ lib/lp/registry/vocabularies.py 2011-08-22 13:16:25 +0000
@@ -292,7 +292,7 @@
raise LookupError(token)
return self.toTerm(product)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""See `SQLObjectVocabularyBase`.
Returns products where the product name, displayname, title,
@@ -345,7 +345,7 @@
raise LookupError(token)
return self.toTerm(project)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""See `SQLObjectVocabularyBase`.
Returns projects where the project name, displayname, title,
@@ -423,7 +423,7 @@
"""Return `IPerson` objects that match the text."""
return getUtility(IPersonSet).find(text)
- def search(self, text):
+ def search(self, text, vocab_filter=None):
"""See `SQLObjectVocabularyBase`.
Return people/teams whose fti or email address match :text.
@@ -457,7 +457,7 @@
text, exclude_inactive_accounts=False,
must_have_email=self.must_have_email)
- def search(self, text):
+ def search(self, text, vocab_filter=None):
"""See `SQLObjectVocabularyBase`.
Return people whose fti or email address match :text.
@@ -478,6 +478,32 @@
must_have_email = False
+class VocabularyFilterPerson(VocabularyFilter):
+ # A filter returning just persons.
+
+ def __new__(cls):
+ return super(VocabularyFilter, cls).__new__(
+ cls, 'PERSON', 'Person',
+ 'Display search results for people only')
+
+ @property
+ def filter_terms(self):
+ return [Person.teamownerID == None]
+
+
+class VocabularyFilterTeam(VocabularyFilter):
+ # A filter returning just teams.
+
+ def __new__(cls):
+ return super(VocabularyFilter, cls).__new__(
+ cls, 'TEAM', 'Team',
+ 'Display search results for teams only')
+
+ @property
+ def filter_terms(self):
+ return [Person.teamownerID != None]
+
+
class ValidPersonOrTeamVocabulary(
BasePersonVocabulary, SQLObjectVocabularyBase):
"""The set of valid, viewable Persons/Teams in Launchpad.
@@ -508,6 +534,9 @@
LIMIT = 100
+ PERSON_FILTER = VocabularyFilterPerson()
+ TEAM_FILTER = VocabularyFilterTeam()
+
def __contains__(self, obj):
return obj in self._doSearch()
@@ -556,19 +585,22 @@
private_query = False
return (private_query, tables)
- def _doSearch(self, text=""):
+ def _doSearch(self, text="", vocab_filter=None):
"""Return the people/teams whose fti or email address match :text:"""
if self.enhanced_picker_enabled:
- return self._doSearchWithImprovedSorting(text)
+ return self._doSearchWithImprovedSorting(text, vocab_filter)
else:
- return self._doSearchWithOriginalSorting(text)
+ return self._doSearchWithOriginalSorting(text, vocab_filter)
- def _doSearchWithOriginalSorting(self, text=""):
+ def _doSearchWithOriginalSorting(self, text="", vocab_filter=None):
private_query, private_tables = self._privateTeamQueryAndTables()
exact_match = None
+ extra_clauses = [self.extra_clause]
+ if vocab_filter:
+ extra_clauses.extend(vocab_filter.filter_terms)
# Short circuit if there is no search text - all valid people and
- # teams have been requested.
+ # teams have been requested. We still honour the vocab filter.
if not text:
tables = [
Person,
@@ -583,7 +615,7 @@
private_query,
),
Person.merged == None,
- self.extra_clause
+ *extra_clauses
)
)
else:
@@ -683,7 +715,7 @@
exact_match = (Person.name == text)
result = self.store.using(subselect).find(
(Person, exact_match),
- self.extra_clause)
+ *extra_clauses)
# XXX: BradCrittenden 2009-05-07 bug=373228: A bug in Storm prevents
# setting the 'distinct' and 'limit' options in a single call to
# .config(). The work-around is to split them up. Note the limit has
@@ -704,13 +736,16 @@
result.config(limit=self.LIMIT)
return result
- def _doSearchWithImprovedSorting(self, text=""):
+ def _doSearchWithImprovedSorting(self, text="", vocab_filter=None):
"""Return the people/teams whose fti or email address match :text:"""
private_query, private_tables = self._privateTeamQueryAndTables()
+ extra_clauses = [self.extra_clause]
+ if vocab_filter:
+ extra_clauses.extend(vocab_filter.filter_terms)
- # Short circuit if there is no search text - all valid people and
- # teams have been requested.
+ # Short circuit if there is no search text - all valid people and/or
+ # teams have been requested. We still honour the vocab filter.
if not text:
tables = [
Person,
@@ -725,7 +760,7 @@
private_query,
),
Person.merged == None,
- self.extra_clause
+ *extra_clauses
)
)
result.config(distinct=True)
@@ -852,7 +887,7 @@
EmailAddressStatus.PREFERRED)),
# Or a private team
private_teams_query),
- self.extra_clause),
+ *extra_clauses),
)
# Better ranked matches go first.
if (getFeatureFlag('disclosure.person_affiliation_rank.enabled')
@@ -893,7 +928,7 @@
return DecoratedResultSet(result, pre_iter_hook=pre_iter_hook)
- def search(self, text):
+ def search(self, text, vocab_filter=None):
"""Return people/teams whose fti or email address match :text:."""
if not text:
if self.allow_null_search:
@@ -902,13 +937,17 @@
return self.emptySelectResults()
text = ensure_unicode(text).lower()
- return self._doSearch(text=text)
+ return self._doSearch(text=text, vocab_filter=vocab_filter)
def searchForTerms(self, query=None, vocab_filter=None):
"""See `IHugeVocabulary`."""
- results = self.search(query)
+ results = self.search(query, vocab_filter)
return CountableIterator(results.count(), results, self.toTerm)
+ def supportedFilters(self):
+ """See `IHugeVocabulary`."""
+ return [self.ALL_FILTER, self.PERSON_FILTER, self.TEAM_FILTER]
+
class ValidTeamVocabulary(ValidPersonOrTeamVocabulary):
"""The set of all valid, public teams in Launchpad."""
@@ -922,7 +961,7 @@
# Search with empty string returns all teams.
allow_null_search = True
- def _doSearch(self, text=""):
+ def _doSearch(self, text="", vocab_filter=None):
"""Return the teams whose fti, IRC, or email address match :text:"""
private_query, private_tables = self._privateTeamQueryAndTables()
@@ -974,6 +1013,9 @@
result.config(limit=self.LIMIT)
return result
+ def supportedFilters(self):
+ return []
+
class ValidPersonVocabulary(ValidPersonOrTeamVocabulary):
"""The set of all valid persons who are not teams in Launchpad."""
@@ -986,6 +1028,9 @@
# Cache table to use for checking validity.
cache_table_name = 'ValidPersonCache'
+ def supportedFilters(self):
+ return []
+
class TeamVocabularyMixin:
"""Common methods for team vocabularies."""
@@ -1202,7 +1247,7 @@
raise LookupError(token)
return self.getTerm(team_list)
- def search(self, text=None):
+ def search(self, text=None, vocab_filter=None):
"""Search for active mailing lists.
:param text: The name of a mailing list, which can be a partial
@@ -1327,7 +1372,7 @@
except IndexError:
raise LookupError(token)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Return terms where query is a substring of the version or name"""
if not query:
return self.emptySelectResults()
@@ -1383,7 +1428,7 @@
return self.toTerm(result)
raise LookupError(token)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Return terms where query is a substring of the name."""
if not query:
return self.emptySelectResults()
@@ -1682,7 +1727,7 @@
else:
return self.toTerm(obj)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Return terms where query is a substring of the name"""
if not query:
return self.emptySelectResults()
@@ -1735,7 +1780,7 @@
else:
return self.toTerm(obj)
- def search(self, query):
+ def search(self, query, vocab_filter=None):
"""Return terms where query is a substring of the name."""
if not query:
return self.emptySelectResults()
=== modified file 'lib/lp/registry/vocabularies.zcml'
--- lib/lp/registry/vocabularies.zcml 2011-08-18 08:00:28 +0000
+++ lib/lp/registry/vocabularies.zcml 2011-08-22 13:16:25 +0000
@@ -487,4 +487,16 @@
permission="zope.Public"
attributes="name title description"/>
</class>
+ <class
+ class="lp.registry.vocabularies.VocabularyFilterPerson">
+ <require
+ permission="zope.Public"
+ attributes="name title description"/>
+ </class>
+ <class
+ class="lp.registry.vocabularies.VocabularyFilterTeam">
+ <require
+ permission="zope.Public"
+ attributes="name title description"/>
+ </class>
</configure>