~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/app/browser/vocabulary.py

[r=gmb][bug=823033] Refactor the vocab term picker entry adapters to
        provide a set based interface,
        processing multiple people in a single call

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
 
8
8
__all__ = [
9
9
    'HugeVocabularyJSONView',
10
 
    'IPickerEntry',
 
10
    'IPickerEntrySource',
11
11
    'get_person_picker_entry_metadata',
12
12
    ]
13
13
 
14
14
import simplejson
 
15
from itertools import izip
15
16
 
16
17
from lazr.restful.interfaces import IWebServiceClientRequest
17
18
from zope.app.form.interfaces import MissingInputError
88
89
        self.metadata = metadata
89
90
 
90
91
 
 
92
class IPickerEntrySource(Interface):
 
93
    """An adapter used to convert vocab terms to picker entries."""
 
94
 
 
95
    def getPickerEntries(term_values, context_object, **kwarg):
 
96
        """Return picker entries for the specified term values.
 
97
 
 
98
        :param term_values: a collection of vocab term values
 
99
        :param context_object: the current context used to determine any
 
100
            affiliation for the resulting picker entries. eg a picker used to
 
101
            select a bug task assignee will have context_object set to the bug
 
102
            task.
 
103
        """
 
104
 
 
105
 
91
106
@adapter(Interface)
92
 
class DefaultPickerEntryAdapter(object):
93
 
    """Adapts Interface to IPickerEntry."""
 
107
class DefaultPickerEntrySourceAdapter(object):
 
108
    """Adapts Interface to IPickerEntrySource."""
94
109
 
95
 
    implements(IPickerEntry)
 
110
    implements(IPickerEntrySource)
96
111
 
97
112
    def __init__(self, context):
98
113
        self.context = context
99
114
 
100
 
    def getPickerEntry(self, associated_object, **kwarg):
101
 
        """ Construct a PickerEntry for the context of this adapter.
102
 
 
103
 
        The associated_object represents the context for which the picker is
104
 
        being rendered. eg a picker used to select a bug task assignee will
105
 
        have associated_object set to the bug task.
106
 
        """
107
 
        extra = PickerEntry()
108
 
        if hasattr(self.context, 'summary'):
109
 
            extra.description = self.context.summary
110
 
        display_api = ObjectImageDisplayAPI(self.context)
111
 
        extra.css = display_api.sprite_css()
112
 
        if extra.css is None:
113
 
            extra.css = 'sprite bullet'
114
 
        return extra
 
115
    def getPickerEntries(self, term_values, context_object, **kwarg):
 
116
        """See `IPickerEntrySource`"""
 
117
        entries = []
 
118
        for term_value in term_values:
 
119
            extra = PickerEntry()
 
120
            if hasattr(term_value, 'summary'):
 
121
                extra.description = term_value.summary
 
122
            display_api = ObjectImageDisplayAPI(term_value)
 
123
            extra.css = display_api.sprite_css()
 
124
            if extra.css is None:
 
125
                extra.css = 'sprite bullet'
 
126
            entries.append(extra)
 
127
        return entries
115
128
 
116
129
 
117
130
def get_person_picker_entry_metadata(picker_entry):
122
135
 
123
136
 
124
137
@adapter(IPerson)
125
 
class PersonPickerEntryAdapter(DefaultPickerEntryAdapter):
126
 
    """Adapts IPerson to IPickerEntry."""
 
138
class PersonPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
 
139
    """Adapts IPerson to IPickerEntrySource."""
127
140
 
128
 
    def getPickerEntry(self, associated_object, **kwarg):
129
 
        person = self.context
130
 
        extra = super(PersonPickerEntryAdapter, self).getPickerEntry(
131
 
            associated_object)
 
141
    def getPickerEntries(self, term_values, context_object, **kwarg):
 
142
        """See `IPickerEntrySource`"""
 
143
        picker_entries = (
 
144
            super(PersonPickerEntrySourceAdapter, self)
 
145
                .getPickerEntries(term_values, context_object))
132
146
 
133
147
        personpicker_affiliation_enabled = kwarg.get(
134
148
                                    'personpicker_affiliation_enabled', False)
135
149
        if personpicker_affiliation_enabled:
136
 
            # If the person is affiliated with the associated_object then we
 
150
            # If a person is affiliated with the associated_object then we
137
151
            # can display a badge.
138
 
            badge_info = IHasAffiliation(
139
 
                associated_object).getAffiliationBadge(person)
140
 
            if badge_info:
141
 
                extra.badges = [
142
 
                    dict(url=badge_info.url, alt=badge_info.alt_text)]
 
152
            badges = IHasAffiliation(
 
153
                context_object).getAffiliationBadges(term_values)
 
154
            for picker_entry, badge_info in izip(picker_entries, badges):
 
155
                if badge_info:
 
156
                    picker_entry.badges = [
 
157
                        dict(url=badge_info.url, alt=badge_info.alt_text)]
143
158
 
144
159
        picker_expander_enabled = kwarg.get('picker_expander_enabled', False)
145
 
        if picker_expander_enabled:
146
 
            extra.details = []
147
 
 
148
 
        if person.preferredemail is not None:
149
 
            if person.hide_email_addresses:
150
 
                extra.description = '<email address hidden>'
151
 
            else:
152
 
                try:
153
 
                    extra.description = person.preferredemail.email
154
 
                except Unauthorized:
155
 
                    extra.description = '<email address hidden>'
156
 
 
157
 
        extra.metadata = get_person_picker_entry_metadata(person)
158
 
        enhanced_picker_enabled = kwarg.get('enhanced_picker_enabled', False)
159
 
        if enhanced_picker_enabled:
160
 
            # We will display the person's name (launchpad id) after their
161
 
            # displayname.
162
 
            extra.alt_title = person.name
163
 
            # We will linkify the person's name so it can be clicked to open
164
 
            # the page for that person.
165
 
            extra.alt_title_link = canonical_url(person, rootsite='mainsite')
166
 
            # We will display the person's irc nick(s) after their email
167
 
            # address in the description text.
168
 
            irc_nicks = None
169
 
            if person.ircnicknames:
170
 
                irc_nicks = ", ".join(
171
 
                    [IRCNicknameFormatterAPI(ircid).displayname()
172
 
                    for ircid in person.ircnicknames])
173
 
            if irc_nicks and not picker_expander_enabled:
174
 
                if extra.description:
175
 
                    extra.description = ("%s (%s)" %
176
 
                        (extra.description, irc_nicks))
177
 
                else:
178
 
                    extra.description = "%s" % irc_nicks
 
160
        for person, picker_entry in izip(term_values, picker_entries):
179
161
            if picker_expander_enabled:
180
 
                if irc_nicks:
181
 
                    extra.details.append(irc_nicks)
182
 
                if person.is_team:
183
 
                    extra.details.append(
184
 
                        'Team members: %s' % person.all_member_count)
 
162
                picker_entry.details = []
 
163
 
 
164
            if person.preferredemail is not None:
 
165
                if person.hide_email_addresses:
 
166
                    picker_entry.description = '<email address hidden>'
185
167
                else:
186
 
                    extra.details.append(
187
 
                        'Member since %s' % DateTimeFormatterAPI(
188
 
                            person.datecreated).date())
 
168
                    try:
 
169
                        picker_entry.description = person.preferredemail.email
 
170
                    except Unauthorized:
 
171
                        picker_entry.description = '<email address hidden>'
189
172
 
190
 
        return extra
 
173
            picker_entry.metadata = get_person_picker_entry_metadata(person)
 
174
            enhanced_picker_enabled = kwarg.get(
 
175
                                            'enhanced_picker_enabled', False)
 
176
            if enhanced_picker_enabled:
 
177
                # We will display the person's name (launchpad id) after their
 
178
                # displayname.
 
179
                picker_entry.alt_title = person.name
 
180
                # We will linkify the person's name so it can be clicked to
 
181
                # open the page for that person.
 
182
                picker_entry.alt_title_link = canonical_url(
 
183
                                                person, rootsite='mainsite')
 
184
                # We will display the person's irc nick(s) after their email
 
185
                # address in the description text.
 
186
                irc_nicks = None
 
187
                if person.ircnicknames:
 
188
                    irc_nicks = ", ".join(
 
189
                        [IRCNicknameFormatterAPI(ircid).displayname()
 
190
                        for ircid in person.ircnicknames])
 
191
                if irc_nicks and not picker_expander_enabled:
 
192
                    if picker_entry.description:
 
193
                        picker_entry.description = ("%s (%s)" %
 
194
                            (picker_entry.description, irc_nicks))
 
195
                    else:
 
196
                        picker_entry.description = "%s" % irc_nicks
 
197
                if picker_expander_enabled:
 
198
                    if irc_nicks:
 
199
                        picker_entry.details.append(irc_nicks)
 
200
                    if person.is_team:
 
201
                        picker_entry.details.append(
 
202
                            'Team members: %s' % person.all_member_count)
 
203
                    else:
 
204
                        picker_entry.details.append(
 
205
                            'Member since %s' % DateTimeFormatterAPI(
 
206
                                person.datecreated).date())
 
207
        return picker_entries
191
208
 
192
209
 
193
210
@adapter(IBranch)
194
 
class BranchPickerEntryAdapter(DefaultPickerEntryAdapter):
195
 
    """Adapts IBranch to IPickerEntry."""
 
211
class BranchPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
 
212
    """Adapts IBranch to IPickerEntrySource."""
196
213
 
197
 
    def getPickerEntry(self, associated_object, **kwarg):
198
 
        branch = self.context
199
 
        extra = super(BranchPickerEntryAdapter, self).getPickerEntry(
200
 
            associated_object)
201
 
        extra.description = branch.bzr_identity
202
 
        return extra
 
214
    def getPickerEntries(self, term_values, context_object, **kwarg):
 
215
        """See `IPickerEntrySource`"""
 
216
        entries = (
 
217
            super(BranchPickerEntrySourceAdapter, self)
 
218
                    .getPickerEntries(term_values, context_object, **kwarg))
 
219
        for branch, picker_entry in izip(term_values, entries):
 
220
            picker_entry.description = branch.bzr_identity
 
221
        return entries
203
222
 
204
223
 
205
224
@adapter(ISourcePackageName)
206
 
class SourcePackageNamePickerEntryAdapter(DefaultPickerEntryAdapter):
207
 
    """Adapts ISourcePackageName to IPickerEntry."""
 
225
class SourcePackageNamePickerEntrySourceAdapter(
 
226
                                            DefaultPickerEntrySourceAdapter):
 
227
    """Adapts ISourcePackageName to IPickerEntrySource."""
208
228
 
209
 
    def getPickerEntry(self, associated_object, **kwarg):
210
 
        sourcepackagename = self.context
211
 
        extra = super(
212
 
            SourcePackageNamePickerEntryAdapter, self).getPickerEntry(
213
 
                associated_object)
214
 
        descriptions = getSourcePackageDescriptions([sourcepackagename])
215
 
        extra.description = descriptions.get(
216
 
            sourcepackagename.name, "Not yet built")
217
 
        return extra
 
229
    def getPickerEntry(self, term_values, context_object, **kwarg):
 
230
        """See `IPickerEntrySource`"""
 
231
        entries = (
 
232
            super(SourcePackageNamePickerEntrySourceAdapter, self)
 
233
                .getPickerEntries(term_values, context_object, **kwarg))
 
234
        for sourcepackagename, picker_entry in izip(term_values, entries):
 
235
            descriptions = getSourcePackageDescriptions([sourcepackagename])
 
236
            picker_entry.description = descriptions.get(
 
237
                sourcepackagename.name, "Not yet built")
 
238
        return entries
218
239
 
219
240
 
220
241
@adapter(IArchive)
221
 
class ArchivePickerEntryAdapter(DefaultPickerEntryAdapter):
222
 
    """Adapts IArchive to IPickerEntry."""
 
242
class ArchivePickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
 
243
    """Adapts IArchive to IPickerEntrySource."""
223
244
 
224
 
    def getPickerEntry(self, associated_object, **kwarg):
225
 
        archive = self.context
226
 
        extra = super(ArchivePickerEntryAdapter, self).getPickerEntry(
227
 
            associated_object)
228
 
        extra.description = '%s/%s' % (archive.owner.name, archive.name)
229
 
        return extra
 
245
    def getPickerEntry(self, term_values, context_object, **kwarg):
 
246
        """See `IPickerEntrySource`"""
 
247
        entries = (
 
248
            super(ArchivePickerEntrySourceAdapter, self)
 
249
                    .getPickerEntries(term_values, context_object, **kwarg))
 
250
        for archive, picker_entry in izip(term_values, entries):
 
251
            picker_entry.description = '%s/%s' % (
 
252
                                       archive.owner.name, archive.name)
 
253
        return entries
230
254
 
231
255
 
232
256
class HugeVocabularyJSONView:
273
297
 
274
298
        batch_navigator = BatchNavigator(matches, self.request)
275
299
 
 
300
        # We need to collate what IPickerEntrySource adapters are required for
 
301
        # the items in the current batch. We expect that the batch will be
 
302
        # homogenous and so only one adapter instance is required, but we
 
303
        # allow for the case where the batch may contain disparate entries
 
304
        # requiring different adapter implementations.
 
305
 
 
306
        # A mapping from adapter class name -> adapter instance
 
307
        adapter_cache = {}
 
308
        # A mapping from adapter class name -> list of vocab terms
 
309
        picker_entry_terms = {}
 
310
        for term in batch_navigator.currentBatch():
 
311
            picker_entry_source = IPickerEntrySource(term.value)
 
312
            adapter_class = picker_entry_source.__class__.__name__
 
313
            picker_terms = picker_entry_terms.get(adapter_class)
 
314
            if picker_terms is None:
 
315
                picker_terms = []
 
316
                picker_entry_terms[adapter_class] = picker_terms
 
317
                adapter_cache[adapter_class] = picker_entry_source
 
318
            picker_terms.append(term.value)
 
319
 
 
320
        # A mapping from vocab terms -> picker entries
 
321
        picker_term_entries = {}
 
322
 
 
323
        # For the list of terms associated with a picker adapter, we get the
 
324
        # corresponding picker entries by calling the adapter.
 
325
        for adapter_class, term_values in picker_entry_terms.items():
 
326
            picker_entries = adapter_cache[adapter_class].getPickerEntries(
 
327
                term_values,
 
328
                self.context,
 
329
                enhanced_picker_enabled=self.enhanced_picker_enabled,
 
330
                picker_expander_enabled=self.picker_expander_enabled,
 
331
                personpicker_affiliation_enabled=
 
332
                    self.personpicker_affiliation_enabled)
 
333
            for term_value, picker_entry in izip(term_values, picker_entries):
 
334
                picker_term_entries[term_value] = picker_entry
 
335
 
276
336
        result = []
277
337
        for term in batch_navigator.currentBatch():
278
338
            entry = dict(value=term.token, title=term.title)
288
348
                # needed for inplace editing via a REST call. The
289
349
                # form picker doesn't need the api_url.
290
350
                entry['api_uri'] = 'Could not find canonical url.'
291
 
            picker_entry = IPickerEntry(term.value).getPickerEntry(
292
 
                self.context,
293
 
                enhanced_picker_enabled=self.enhanced_picker_enabled,
294
 
                picker_expander_enabled=self.picker_expander_enabled,
295
 
                personpicker_affiliation_enabled=
296
 
                    self.personpicker_affiliation_enabled)
 
351
            picker_entry = picker_term_entries[term.value]
297
352
            if picker_entry.description is not None:
298
353
                if len(picker_entry.description) > MAX_DESCRIPTION_LENGTH:
299
354
                    entry['description'] = (