~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Steve Kowalik
  • Date: 2011-08-07 04:05:52 UTC
  • mto: This revision was merged to the branch mainline in revision 13626.
  • Revision ID: stevenk@ubuntu.com-20110807040552-mwnxo0flmhvl35e8
Correct the notification based on review comments, and remove request{,ed}
from the function names, switching to create{,d}.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 
1
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
3
 
4
4
"""Views which export vocabularies as JSON for widgets."""
7
7
 
8
8
__all__ = [
9
9
    'HugeVocabularyJSONView',
10
 
    'IPickerEntrySource',
 
10
    'IPickerEntry',
11
11
    'get_person_picker_entry_metadata',
12
12
    ]
13
13
 
14
 
from itertools import izip
 
14
import simplejson
15
15
 
16
16
from lazr.restful.interfaces import IWebServiceClientRequest
17
 
import simplejson
18
17
from zope.app.form.interfaces import MissingInputError
19
18
from zope.app.schema.vocabulary import IVocabularyFactory
20
19
from zope.component import (
29
28
    )
30
29
from zope.security.interfaces import Unauthorized
31
30
 
 
31
from canonical.launchpad.webapp.batching import BatchNavigator
 
32
from canonical.launchpad.webapp.interfaces import NoCanonicalUrl
 
33
from canonical.launchpad.webapp.publisher import canonical_url
32
34
from lp.app.browser.tales import (
33
35
    DateTimeFormatterAPI,
34
36
    IRCNicknameFormatterAPI,
35
37
    ObjectImageDisplayAPI,
36
38
    )
 
39
from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
37
40
from lp.app.errors import UnexpectedFormData
38
41
from lp.code.interfaces.branch import IBranch
39
 
from lp.registry.interfaces.distribution import IDistribution
40
 
from lp.registry.interfaces.distributionsourcepackage import (
41
 
    IDistributionSourcePackage,
42
 
    )
43
42
from lp.registry.interfaces.person import IPerson
44
 
from lp.registry.interfaces.product import IProduct
45
 
from lp.registry.interfaces.projectgroup import IProjectGroup
46
43
from lp.registry.interfaces.sourcepackagename import ISourcePackageName
47
44
from lp.registry.model.pillaraffiliation import IHasAffiliation
48
45
from lp.registry.model.sourcepackagename import getSourcePackageDescriptions
49
 
from lp.services.webapp.batching import BatchNavigator
50
 
from lp.services.webapp.interfaces import NoCanonicalUrl
51
 
from lp.services.webapp.publisher import canonical_url
52
 
from lp.services.webapp.vocabulary import IHugeVocabulary
 
46
from lp.services.features import getFeatureFlag
53
47
from lp.soyuz.interfaces.archive import IArchive
54
48
 
55
49
# XXX: EdwinGrubbs 2009-07-27 bug=405476
73
67
    link_css = Attribute('CSS Class for links')
74
68
    badges = Attribute('List of badge img attributes')
75
69
    metadata = Attribute('Metadata about the entry')
76
 
    target_type = Attribute('Target data for target picker entries.')
77
70
 
78
71
 
79
72
class PickerEntry:
82
75
 
83
76
    def __init__(self, description=None, image=None, css=None, alt_title=None,
84
77
                 title_link=None, details=None, alt_title_link=None,
85
 
                 link_css='sprite new-window', badges=None, metadata=None,
86
 
                 target_type=None):
 
78
                 link_css='sprite new-window', badges=None, metadata=None):
87
79
        self.description = description
88
80
        self.image = image
89
81
        self.css = css
94
86
        self.link_css = link_css
95
87
        self.badges = badges
96
88
        self.metadata = metadata
97
 
        self.target_type = target_type
98
 
 
99
 
 
100
 
class IPickerEntrySource(Interface):
101
 
    """An adapter used to convert vocab terms to picker entries."""
102
 
 
103
 
    def getPickerEntries(term_values, context_object, **kwarg):
104
 
        """Return picker entries for the specified term values.
105
 
 
106
 
        :param term_values: a collection of vocab term values
107
 
        :param context_object: the current context used to determine any
108
 
            affiliation for the resulting picker entries. eg a picker used to
109
 
            select a bug task assignee will have context_object set to the bug
110
 
            task.
111
 
        """
112
89
 
113
90
 
114
91
@adapter(Interface)
115
 
class DefaultPickerEntrySourceAdapter(object):
116
 
    """Adapts Interface to IPickerEntrySource."""
 
92
class DefaultPickerEntryAdapter(object):
 
93
    """Adapts Interface to IPickerEntry."""
117
94
 
118
 
    implements(IPickerEntrySource)
 
95
    implements(IPickerEntry)
119
96
 
120
97
    def __init__(self, context):
121
98
        self.context = context
122
99
 
123
 
    def getPickerEntries(self, term_values, context_object, **kwarg):
124
 
        """See `IPickerEntrySource`"""
125
 
        entries = []
126
 
        for term_value in term_values:
127
 
            extra = PickerEntry()
128
 
            if hasattr(term_value, 'summary'):
129
 
                extra.description = term_value.summary
130
 
            display_api = ObjectImageDisplayAPI(term_value)
131
 
            image_url = display_api.custom_icon_url() or None
132
 
            css = display_api.sprite_css() or 'sprite bullet'
133
 
            if image_url is not None:
134
 
                extra.image = image_url
135
 
            else:
136
 
                extra.css = css
137
 
            entries.append(extra)
138
 
        return entries
 
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
139
115
 
140
116
 
141
117
def get_person_picker_entry_metadata(picker_entry):
146
122
 
147
123
 
148
124
@adapter(IPerson)
149
 
class PersonPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
150
 
    """Adapts IPerson to IPickerEntrySource."""
151
 
 
152
 
    def getPickerEntries(self, term_values, context_object, **kwarg):
153
 
        """See `IPickerEntrySource`"""
154
 
        picker_entries = (
155
 
            super(PersonPickerEntrySourceAdapter, self)
156
 
                .getPickerEntries(term_values, context_object))
157
 
 
158
 
        affiliated_context = IHasAffiliation(context_object, None)
159
 
        if affiliated_context is not None:
160
 
            # If a person is affiliated with the associated_object then we
 
125
class PersonPickerEntryAdapter(DefaultPickerEntryAdapter):
 
126
    """Adapts IPerson to IPickerEntry."""
 
127
 
 
128
    def getPickerEntry(self, associated_object, **kwarg):
 
129
        person = self.context
 
130
        extra = super(PersonPickerEntryAdapter, self).getPickerEntry(
 
131
            associated_object)
 
132
 
 
133
        enhanced_picker_enabled = kwarg.get('enhanced_picker_enabled', False)
 
134
        if enhanced_picker_enabled:
 
135
            # If the person is affiliated with the associated_object then we
161
136
            # can display a badge.
162
 
            badges = affiliated_context.getAffiliationBadges(term_values)
163
 
            for picker_entry, badges in izip(picker_entries, badges):
164
 
                picker_entry.badges = []
165
 
                for badge_info in badges:
166
 
                    picker_entry.badges.append(
167
 
                        dict(url=badge_info.url,
168
 
                             label=badge_info.label,
169
 
                             role=badge_info.role))
170
 
 
171
 
        for person, picker_entry in izip(term_values, picker_entries):
172
 
            picker_entry.details = []
173
 
 
174
 
            if person.preferredemail is not None:
175
 
                if person.hide_email_addresses:
176
 
                    picker_entry.description = '<email address hidden>'
177
 
                else:
178
 
                    try:
179
 
                        picker_entry.description = person.preferredemail.email
180
 
                    except Unauthorized:
181
 
                        picker_entry.description = '<email address hidden>'
182
 
 
183
 
            picker_entry.metadata = get_person_picker_entry_metadata(person)
 
137
            badge_info = IHasAffiliation(
 
138
                associated_object).getAffiliationBadge(person)
 
139
            if badge_info:
 
140
                extra.badges = [
 
141
                    dict(url=badge_info.url, alt=badge_info.alt_text)]
 
142
 
 
143
        picker_expander_enabled = kwarg.get('picker_expander_enabled', False)
 
144
        if picker_expander_enabled:
 
145
            extra.details = []
 
146
 
 
147
        if person.preferredemail is not None:
 
148
            if person.hide_email_addresses:
 
149
                extra.description = '<email address hidden>'
 
150
            else:
 
151
                try:
 
152
                    extra.description = person.preferredemail.email
 
153
                except Unauthorized:
 
154
                    extra.description = '<email address hidden>'
 
155
 
 
156
        extra.metadata = get_person_picker_entry_metadata(person)
 
157
        if enhanced_picker_enabled:
184
158
            # We will display the person's name (launchpad id) after their
185
159
            # displayname.
186
 
            picker_entry.alt_title = person.name
187
 
            # We will linkify the person's name so it can be clicked to
188
 
            # open the page for that person.
189
 
            picker_entry.alt_title_link = canonical_url(
190
 
                                            person, rootsite='mainsite')
 
160
            extra.alt_title = person.name
 
161
            # We will linkify the person's name so it can be clicked to open
 
162
            # the page for that person.
 
163
            extra.alt_title_link = canonical_url(person, rootsite='mainsite')
191
164
            # We will display the person's irc nick(s) after their email
192
165
            # address in the description text.
193
166
            irc_nicks = None
195
168
                irc_nicks = ", ".join(
196
169
                    [IRCNicknameFormatterAPI(ircid).displayname()
197
170
                    for ircid in person.ircnicknames])
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
 
171
            if irc_nicks and not picker_expander_enabled:
 
172
                if extra.description:
 
173
                    extra.description = ("%s (%s)" %
 
174
                        (extra.description, irc_nicks))
 
175
                else:
 
176
                    extra.description = "%s" % irc_nicks
 
177
            if picker_expander_enabled:
 
178
                if irc_nicks:
 
179
                    extra.details.append(irc_nicks)
 
180
                if person.is_team:
 
181
                    extra.details.append(
 
182
                        'Team members: %s' % person.all_member_count)
 
183
                else:
 
184
                    extra.details.append(
 
185
                        'Member since %s' % DateTimeFormatterAPI(
 
186
                            person.datecreated).date())
 
187
 
 
188
        return extra
208
189
 
209
190
 
210
191
@adapter(IBranch)
211
 
class BranchPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
212
 
    """Adapts IBranch to IPickerEntrySource."""
213
 
 
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
222
 
 
223
 
 
224
 
class TargetPickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
225
 
    """Adapt targets (Product, Package, Distribution) to PickerEntrySource."""
226
 
 
227
 
    target_type = ""
228
 
 
229
 
    def getDescription(self, target):
230
 
        """Gets the description data for target picker entries."""
231
 
        raise NotImplemented
232
 
 
233
 
    def getMaintainer(self, target):
234
 
        """Gets the maintainer information for the target picker entry."""
235
 
        raise NotImplemented
236
 
 
237
 
    def getPickerEntries(self, term_values, context_object, **kwarg):
238
 
        """See `IPickerEntrySource`"""
239
 
        entries = (
240
 
            super(TargetPickerEntrySourceAdapter, self)
241
 
                .getPickerEntries(term_values, context_object, **kwarg))
242
 
        for target, picker_entry in izip(term_values, entries):
243
 
            picker_entry.description = self.getDescription(target)
244
 
            picker_entry.details = []
245
 
            summary = picker_entry.description
246
 
            if len(summary) > 45:
247
 
                index = summary.rfind(' ', 0, 45)
248
 
                first_line = summary[0:index + 1]
249
 
                second_line = summary[index:]
250
 
            else:
251
 
                first_line = summary
252
 
                second_line = ''
253
 
 
254
 
            if len(second_line) > 90:
255
 
                index = second_line.rfind(' ', 0, 90)
256
 
                second_line = second_line[0:index + 1]
257
 
            picker_entry.description = first_line
258
 
            if second_line:
259
 
                picker_entry.details.append(second_line)
260
 
            picker_entry.alt_title = target.name
261
 
            picker_entry.alt_title_link = canonical_url(
262
 
                target, rootsite='mainsite')
263
 
            picker_entry.target_type = self.target_type
264
 
            maintainer = self.getMaintainer(target)
265
 
            if maintainer is not None:
266
 
                picker_entry.details.append(
267
 
                    'Maintainer: %s' % self.getMaintainer(target))
268
 
        return entries
 
192
class BranchPickerEntryAdapter(DefaultPickerEntryAdapter):
 
193
    """Adapts IBranch to IPickerEntry."""
 
194
 
 
195
    def getPickerEntry(self, associated_object, **kwarg):
 
196
        branch = self.context
 
197
        extra = super(BranchPickerEntryAdapter, self).getPickerEntry(
 
198
            associated_object)
 
199
        extra.description = branch.bzr_identity
 
200
        return extra
269
201
 
270
202
 
271
203
@adapter(ISourcePackageName)
272
 
class SourcePackageNamePickerEntrySourceAdapter(
273
 
                                            DefaultPickerEntrySourceAdapter):
274
 
    """Adapts ISourcePackageName to IPickerEntrySource."""
275
 
 
276
 
    def getPickerEntries(self, term_values, context_object, **kwarg):
277
 
        """See `IPickerEntrySource`"""
278
 
        entries = (
279
 
            super(SourcePackageNamePickerEntrySourceAdapter, self)
280
 
                .getPickerEntries(term_values, context_object, **kwarg))
281
 
        for sourcepackagename, picker_entry in izip(term_values, entries):
282
 
            descriptions = getSourcePackageDescriptions([sourcepackagename])
283
 
            picker_entry.description = descriptions.get(
284
 
                sourcepackagename.name, "Not yet built")
285
 
        return entries
286
 
 
287
 
 
288
 
@adapter(IDistributionSourcePackage)
289
 
class DistributionSourcePackagePickerEntrySourceAdapter(
290
 
    TargetPickerEntrySourceAdapter):
291
 
    """Adapts IDistributionSourcePackage to IPickerEntrySource."""
292
 
 
293
 
    target_type = "package"
294
 
 
295
 
    def getMaintainer(self, target):
296
 
        """See `TargetPickerEntrySource`"""
297
 
        return None
298
 
 
299
 
    def getDescription(self, target):
300
 
        """See `TargetPickerEntrySource`"""
301
 
        if target.binary_names:
302
 
            description = ', '.join(target.binary_names)
303
 
        else:
304
 
            description = 'Not yet built.'
305
 
        return description
306
 
 
307
 
    def getPickerEntries(self, term_values, context_object, **kwarg):
308
 
        this = super(DistributionSourcePackagePickerEntrySourceAdapter, self)
309
 
        entries = this.getPickerEntries(term_values, context_object, **kwarg)
310
 
        for picker_entry in entries:
311
 
            picker_entry.alt_title = None
312
 
        return entries
313
 
 
314
 
 
315
 
@adapter(IProjectGroup)
316
 
class ProjectGroupPickerEntrySourceAdapter(TargetPickerEntrySourceAdapter):
317
 
    """Adapts IProduct to IPickerEntrySource."""
318
 
 
319
 
    target_type = "project group"
320
 
 
321
 
    def getMaintainer(self, target):
322
 
        """See `TargetPickerEntrySource`"""
323
 
        return target.owner.displayname
324
 
 
325
 
    def getDescription(self, target):
326
 
        """See `TargetPickerEntrySource`"""
327
 
        return target.summary
328
 
 
329
 
 
330
 
@adapter(IProduct)
331
 
class ProductPickerEntrySourceAdapter(TargetPickerEntrySourceAdapter):
332
 
    """Adapts IProduct to IPickerEntrySource."""
333
 
 
334
 
    target_type = "project"
335
 
 
336
 
    def getMaintainer(self, target):
337
 
        """See `TargetPickerEntrySource`"""
338
 
        return target.owner.displayname
339
 
 
340
 
    def getDescription(self, target):
341
 
        """See `TargetPickerEntrySource`"""
342
 
        return target.summary
343
 
 
344
 
 
345
 
@adapter(IDistribution)
346
 
class DistributionPickerEntrySourceAdapter(TargetPickerEntrySourceAdapter):
347
 
 
348
 
    target_type = "distribution"
349
 
 
350
 
    def getMaintainer(self, target):
351
 
        """See `TargetPickerEntrySource`"""
352
 
        try:
353
 
            return target.currentseries.owner.displayname
354
 
        except AttributeError:
355
 
            return None
356
 
 
357
 
    def getDescription(self, target):
358
 
        """See `TargetPickerEntrySource`"""
359
 
        return target.summary
 
204
class SourcePackageNamePickerEntryAdapter(DefaultPickerEntryAdapter):
 
205
    """Adapts ISourcePackageName to IPickerEntry."""
 
206
 
 
207
    def getPickerEntry(self, associated_object, **kwarg):
 
208
        sourcepackagename = self.context
 
209
        extra = super(
 
210
            SourcePackageNamePickerEntryAdapter, self).getPickerEntry(
 
211
                associated_object)
 
212
        descriptions = getSourcePackageDescriptions([sourcepackagename])
 
213
        extra.description = descriptions.get(
 
214
            sourcepackagename.name, "Not yet built")
 
215
        return extra
360
216
 
361
217
 
362
218
@adapter(IArchive)
363
 
class ArchivePickerEntrySourceAdapter(DefaultPickerEntrySourceAdapter):
364
 
    """Adapts IArchive to IPickerEntrySource."""
 
219
class ArchivePickerEntryAdapter(DefaultPickerEntryAdapter):
 
220
    """Adapts IArchive to IPickerEntry."""
365
221
 
366
 
    def getPickerEntries(self, term_values, context_object, **kwarg):
367
 
        """See `IPickerEntrySource`"""
368
 
        entries = (
369
 
            super(ArchivePickerEntrySourceAdapter, self)
370
 
                    .getPickerEntries(term_values, context_object, **kwarg))
371
 
        for archive, picker_entry in izip(term_values, entries):
372
 
            picker_entry.description = '%s/%s' % (
373
 
                                       archive.owner.name, archive.name)
374
 
        return entries
 
222
    def getPickerEntry(self, associated_object, **kwarg):
 
223
        archive = self.context
 
224
        extra = super(ArchivePickerEntryAdapter, self).getPickerEntry(
 
225
            associated_object)
 
226
        extra.description = '%s/%s' % (archive.owner.name, archive.name)
 
227
        return extra
375
228
 
376
229
 
377
230
class HugeVocabularyJSONView:
385
238
    def __init__(self, context, request):
386
239
        self.context = context
387
240
        self.request = request
 
241
        self.enhanced_picker_enabled = bool(
 
242
            getFeatureFlag('disclosure.picker_enhancements.enabled'))
 
243
        self.picker_expander_enabled = bool(
 
244
            getFeatureFlag('disclosure.picker_expander.enabled'))
388
245
 
389
246
    def __call__(self):
390
247
        name = self.request.form.get('name')
394
251
        search_text = self.request.form.get('search_text')
395
252
        if search_text is None:
396
253
            raise MissingInputError('search_text', '')
397
 
        search_filter = self.request.form.get('search_filter')
398
254
 
399
255
        try:
400
256
            factory = getUtility(IVocabularyFactory, name)
405
261
        vocabulary = factory(self.context)
406
262
 
407
263
        if IHugeVocabulary.providedBy(vocabulary):
408
 
            matches = vocabulary.searchForTerms(search_text, search_filter)
 
264
            matches = vocabulary.searchForTerms(search_text)
409
265
            total_size = matches.count()
410
266
        else:
411
267
            matches = list(vocabulary)
413
269
 
414
270
        batch_navigator = BatchNavigator(matches, self.request)
415
271
 
416
 
        # We need to collate what IPickerEntrySource adapters are required for
417
 
        # the items in the current batch. We expect that the batch will be
418
 
        # homogenous and so only one adapter instance is required, but we
419
 
        # allow for the case where the batch may contain disparate entries
420
 
        # requiring different adapter implementations.
421
 
 
422
 
        # A mapping from adapter class name -> adapter instance
423
 
        adapter_cache = {}
424
 
        # A mapping from adapter class name -> list of vocab terms
425
 
        picker_entry_terms = {}
426
 
        for term in batch_navigator.currentBatch():
427
 
            picker_entry_source = IPickerEntrySource(term.value)
428
 
            adapter_class = picker_entry_source.__class__.__name__
429
 
            picker_terms = picker_entry_terms.get(adapter_class)
430
 
            if picker_terms is None:
431
 
                picker_terms = []
432
 
                picker_entry_terms[adapter_class] = picker_terms
433
 
                adapter_cache[adapter_class] = picker_entry_source
434
 
            picker_terms.append(term.value)
435
 
 
436
 
        # A mapping from vocab terms -> picker entries
437
 
        picker_term_entries = {}
438
 
 
439
 
        # For the list of terms associated with a picker adapter, we get the
440
 
        # corresponding picker entries by calling the adapter.
441
 
        for adapter_class, term_values in picker_entry_terms.items():
442
 
            picker_entries = adapter_cache[adapter_class].getPickerEntries(
443
 
                term_values,
444
 
                self.context)
445
 
            for term_value, picker_entry in izip(term_values, picker_entries):
446
 
                picker_term_entries[term_value] = picker_entry
447
 
 
448
272
        result = []
449
273
        for term in batch_navigator.currentBatch():
450
274
            entry = dict(value=term.token, title=term.title)
460
284
                # needed for inplace editing via a REST call. The
461
285
                # form picker doesn't need the api_url.
462
286
                entry['api_uri'] = 'Could not find canonical url.'
463
 
            picker_entry = picker_term_entries[term.value]
 
287
            picker_entry = IPickerEntry(term.value).getPickerEntry(
 
288
                self.context,
 
289
                enhanced_picker_enabled=self.enhanced_picker_enabled,
 
290
                picker_expander_enabled=self.picker_expander_enabled)
464
291
            if picker_entry.description is not None:
465
292
                if len(picker_entry.description) > MAX_DESCRIPTION_LENGTH:
466
293
                    entry['description'] = (
482
309
                entry['alt_title_link'] = picker_entry.alt_title_link
483
310
            if picker_entry.link_css is not None:
484
311
                entry['link_css'] = picker_entry.link_css
485
 
            if picker_entry.badges:
 
312
            if picker_entry.badges is not None:
486
313
                entry['badges'] = picker_entry.badges
487
314
            if picker_entry.metadata is not None:
488
315
                entry['metadata'] = picker_entry.metadata
489
 
            if picker_entry.target_type is not None:
490
 
                entry['target_type'] = picker_entry.target_type
491
316
            result.append(entry)
492
317
 
493
318
        self.request.response.setHeader('Content-type', 'application/json')