~launchpad-pqm/launchpad/devel

14012.1.2 by Curtis Hovey
Added rudimentary UsesBugsDistributionVocabulary.
1
# Copyright 2011 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Bug domain vocabularies"""
5
6
__metaclass__ = type
14012.1.8 by Curtis Hovey
Created BugTaskTargetWidget which uses UsesBugsDistributionVocabulary.
7
__all__ = [
8
    'UsesBugsDistributionVocabulary',
14435.1.1 by Curtis Hovey
moved bug vocabs to lp.bugs.
9
    'BugNominatableDistroSeriesVocabulary',
10
    'BugNominatableProductSeriesVocabulary',
11
    'BugNominatableSeriesVocabulary',
12
    'BugTrackerVocabulary',
13
    'BugVocabulary',
14
    'BugWatchVocabulary',
15
    'DistributionUsingMaloneVocabulary',
16
    'project_products_using_malone_vocabulary_factory',
17
    'UsesBugsDistributionVocabulary',
18
    'WebBugTrackerVocabulary',
14012.1.8 by Curtis Hovey
Created BugTaskTargetWidget which uses UsesBugsDistributionVocabulary.
19
    ]
14012.1.2 by Curtis Hovey
Added rudimentary UsesBugsDistributionVocabulary.
20
14435.1.1 by Curtis Hovey
moved bug vocabs to lp.bugs.
21
import cgi
22
from operator import attrgetter
23
24
from sqlobject import (
25
    CONTAINSSTRING,
26
    OR,
27
    )
28
from storm.expr import (
29
    And,
30
    Or,
31
    )
32
from zope.component import getUtility
33
from zope.interface import implements
34
from zope.schema.interfaces import (
35
    IVocabulary,
36
    IVocabularyTokenized,
37
    )
38
from zope.schema.vocabulary import (
39
    SimpleTerm,
40
    SimpleVocabulary,
41
    )
42
43
from lp.app.browser.stringformatter import FormattersAPI
44
from lp.app.enums import ServiceUsage
45
from lp.bugs.interfaces.bugtask import IBugTask
46
from lp.bugs.interfaces.bugtracker import BugTrackerType
47
from lp.bugs.model.bug import Bug
48
from lp.bugs.model.bugtracker import BugTracker
49
from lp.bugs.model.bugwatch import BugWatch
14012.1.5 by Curtis Hovey
Adapt to a distribution and do not accept a non-distro.
50
from lp.registry.interfaces.distribution import IDistribution
14435.1.1 by Curtis Hovey
moved bug vocabs to lp.bugs.
51
from lp.registry.interfaces.projectgroup import IProjectGroup
52
from lp.registry.interfaces.series import SeriesStatus
53
from lp.registry.model.distribution import Distribution
54
from lp.registry.model.distroseries import DistroSeries
55
from lp.registry.model.productseries import ProductSeries
14012.1.2 by Curtis Hovey
Added rudimentary UsesBugsDistributionVocabulary.
56
from lp.registry.vocabularies import DistributionVocabulary
14612.2.1 by William Grant
format-imports on lib/. So many imports.
57
from lp.services.database.lpstorm import IStore
58
from lp.services.helpers import (
59
    ensure_unicode,
60
    shortlist,
61
    )
62
from lp.services.webapp.interfaces import ILaunchBag
63
from lp.services.webapp.vocabulary import (
64
    CountableIterator,
65
    IHugeVocabulary,
66
    NamedSQLObjectVocabulary,
67
    SQLObjectVocabularyBase,
68
    )
14012.1.2 by Curtis Hovey
Added rudimentary UsesBugsDistributionVocabulary.
69
70
71
class UsesBugsDistributionVocabulary(DistributionVocabulary):
14012.1.5 by Curtis Hovey
Adapt to a distribution and do not accept a non-distro.
72
    """Distributions that use Launchpad to track bugs.
73
74
    If the context is a distribution, it is always included in the
75
    vocabulary. Historic data is not invalidated if a distro stops
76
    using Launchpad to track bugs. This vocabulary offers the correct
77
    choices of distributions at this moment.
78
    """
79
80
    def __init__(self, context=None):
81
        super(UsesBugsDistributionVocabulary, self).__init__(context=context)
82
        self.distribution = IDistribution(self.context, None)
14012.1.3 by Curtis Hovey
Constrain UsesBugsDistributionVocabulary to distros that use Lp Bugs.
83
84
    @property
85
    def _filter(self):
14012.1.5 by Curtis Hovey
Adapt to a distribution and do not accept a non-distro.
86
        if self.distribution is None:
14012.1.4 by Curtis Hovey
Added the context distribution to the vocab to ensure historic
87
            distro_id = 0
88
        else:
14012.1.5 by Curtis Hovey
Adapt to a distribution and do not accept a non-distro.
89
            distro_id = self.distribution.id
14012.1.4 by Curtis Hovey
Added the context distribution to the vocab to ensure historic
90
        return OR(
91
            self._table.q.official_malone == True,
92
            self._table.id == distro_id)
14435.1.1 by Curtis Hovey
moved bug vocabs to lp.bugs.
93
94
95
class BugVocabulary(SQLObjectVocabularyBase):
96
97
    _table = Bug
98
    _orderBy = 'id'
99
100
101
class BugTrackerVocabulary(SQLObjectVocabularyBase):
102
    """All web and email based external bug trackers."""
103
    displayname = 'Select a bug tracker'
104
    step_title = 'Search'
105
    implements(IHugeVocabulary)
106
    _table = BugTracker
107
    _filter = True
108
    _orderBy = 'title'
109
    _order_by = [BugTracker.title]
110
111
    def toTerm(self, obj):
112
        """See `IVocabulary`."""
113
        return SimpleTerm(obj, obj.name, obj.title)
114
115
    def getTermByToken(self, token):
116
        """See `IVocabularyTokenized`."""
117
        result = IStore(self._table).find(
118
            self._table,
119
            self._filter,
120
            BugTracker.name == token).one()
121
        if result is None:
122
            raise LookupError(token)
123
        return self.toTerm(result)
124
125
    def search(self, query, vocab_filter=None):
126
        """Search for web bug trackers."""
127
        query = ensure_unicode(query).lower()
128
        results = IStore(self._table).find(
129
            self._table, And(
130
            self._filter,
131
            BugTracker.active == True,
132
            Or(
133
                CONTAINSSTRING(BugTracker.name, query),
134
                CONTAINSSTRING(BugTracker.title, query),
135
                CONTAINSSTRING(BugTracker.summary, query),
136
                CONTAINSSTRING(BugTracker.baseurl, query))))
137
        results = results.order_by(self._order_by)
138
        return results
139
140
    def searchForTerms(self, query=None, vocab_filter=None):
141
        """See `IHugeVocabulary`."""
142
        results = self.search(query, vocab_filter)
143
        return CountableIterator(results.count(), results, self.toTerm)
144
145
146
class WebBugTrackerVocabulary(BugTrackerVocabulary):
147
    """All web-based bug tracker types."""
148
    _filter = BugTracker.bugtrackertype != BugTrackerType.EMAILADDRESS
149
150
151
def project_products_using_malone_vocabulary_factory(context):
152
    """Return a vocabulary containing a project's products using Malone."""
153
    project = IProjectGroup(context)
154
    return SimpleVocabulary([
155
        SimpleTerm(product, product.name, title=product.displayname)
156
        for product in project.products
157
        if product.bug_tracking_usage == ServiceUsage.LAUNCHPAD])
158
159
160
class BugWatchVocabulary(SQLObjectVocabularyBase):
161
    _table = BugWatch
162
163
    def __iter__(self):
164
        assert IBugTask.providedBy(self.context), (
165
            "BugWatchVocabulary expects its context to be an IBugTask.")
166
        bug = self.context.bug
167
168
        for watch in bug.watches:
169
            yield self.toTerm(watch)
170
171
    def toTerm(self, watch):
172
173
        def escape(string):
174
            return cgi.escape(string, quote=True)
175
176
        if watch.url.startswith('mailto:'):
177
            user = getUtility(ILaunchBag).user
178
            if user is None:
179
                title = FormattersAPI(
180
                    watch.bugtracker.title).obfuscate_email()
181
                return SimpleTerm(
182
                    watch, watch.id, escape(title))
183
            else:
184
                url = watch.url
185
                title = escape(watch.bugtracker.title)
186
                if url in title:
187
                    title = title.replace(
188
                        url, '<a href="%s">%s</a>' % (
189
                            escape(url), escape(url)))
190
                else:
191
                    title = '%s &lt;<a href="%s">%s</a>&gt;' % (
192
                        title, escape(url), escape(url[7:]))
193
                return SimpleTerm(watch, watch.id, title)
194
        else:
195
            return SimpleTerm(
196
                watch, watch.id, '%s <a href="%s">#%s</a>' % (
197
                    escape(watch.bugtracker.title),
198
                    escape(watch.url),
199
                    escape(watch.remotebug)))
200
201
202
class DistributionUsingMaloneVocabulary:
203
    """All the distributions that uses Malone officially."""
204
205
    implements(IVocabulary, IVocabularyTokenized)
206
207
    _orderBy = 'displayname'
208
209
    def __init__(self, context=None):
210
        self.context = context
211
212
    def __iter__(self):
213
        """Return an iterator which provides the terms from the vocabulary."""
214
        distributions_using_malone = Distribution.selectBy(
215
            official_malone=True, orderBy=self._orderBy)
216
        for distribution in distributions_using_malone:
217
            yield self.getTerm(distribution)
218
219
    def __len__(self):
220
        return Distribution.selectBy(official_malone=True).count()
221
222
    def __contains__(self, obj):
223
        return (IDistribution.providedBy(obj)
224
                and obj.bug_tracking_usage == ServiceUsage.LAUNCHPAD)
225
226
    def getTerm(self, obj):
227
        if obj not in self:
228
            raise LookupError(obj)
229
        return SimpleTerm(obj, obj.name, obj.displayname)
230
231
    def getTermByToken(self, token):
232
        found_dist = Distribution.selectOneBy(
233
            name=token, official_malone=True)
234
        if found_dist is None:
235
            raise LookupError(token)
236
        return self.getTerm(found_dist)
237
238
239
def BugNominatableSeriesVocabulary(context=None):
240
    """Return a nominatable series vocabulary."""
241
    if getUtility(ILaunchBag).distribution:
242
        return BugNominatableDistroSeriesVocabulary(
243
            context, getUtility(ILaunchBag).distribution)
244
    else:
245
        assert getUtility(ILaunchBag).product
246
        return BugNominatableProductSeriesVocabulary(
247
            context, getUtility(ILaunchBag).product)
248
249
250
class BugNominatableSeriesVocabularyBase(NamedSQLObjectVocabulary):
251
    """Base vocabulary class for series for which a bug can be nominated."""
252
253
    def __iter__(self):
254
        bug = self.context.bug
255
256
        all_series = self._getNominatableObjects()
257
258
        for series in sorted(all_series, key=attrgetter("displayname")):
259
            if bug.canBeNominatedFor(series):
260
                yield self.toTerm(series)
261
262
    def toTerm(self, obj):
263
        return SimpleTerm(obj, obj.name, obj.name.capitalize())
264
265
    def getTermByToken(self, token):
266
        obj = self._queryNominatableObjectByName(token)
267
        if obj is None:
268
            raise LookupError(token)
269
270
        return self.toTerm(obj)
271
272
    def _getNominatableObjects(self):
273
        """Return the series objects that the bug can be nominated for."""
274
        raise NotImplementedError
275
276
    def _queryNominatableObjectByName(self, name):
277
        """Return the series object with the given name."""
278
        raise NotImplementedError
279
280
281
class BugNominatableProductSeriesVocabulary(
282
    BugNominatableSeriesVocabularyBase):
283
    """The product series for which a bug can be nominated."""
284
285
    _table = ProductSeries
286
287
    def __init__(self, context, product):
288
        BugNominatableSeriesVocabularyBase.__init__(self, context)
289
        self.product = product
290
291
    def _getNominatableObjects(self):
292
        """See BugNominatableSeriesVocabularyBase."""
293
        return shortlist(self.product.series)
294
295
    def _queryNominatableObjectByName(self, name):
296
        """See BugNominatableSeriesVocabularyBase."""
297
        return self.product.getSeries(name)
298
299
300
class BugNominatableDistroSeriesVocabulary(
301
    BugNominatableSeriesVocabularyBase):
302
    """The distribution series for which a bug can be nominated."""
303
304
    _table = DistroSeries
305
306
    def __init__(self, context, distribution):
307
        BugNominatableSeriesVocabularyBase.__init__(self, context)
308
        self.distribution = distribution
309
310
    def _getNominatableObjects(self):
311
        """Return all non-obsolete distribution series"""
312
        return [
313
            series for series in shortlist(self.distribution.series)
314
            if series.status != SeriesStatus.OBSOLETE]
315
316
    def _queryNominatableObjectByName(self, name):
317
        """See BugNominatableSeriesVocabularyBase."""
318
        return self.distribution.getSeries(name)