~launchpad-pqm/launchpad/devel

13155.1.17 by Curtis Hovey
Updated the copyrights in the very old branch.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.15 by Karl Fogel
Add the copyright header block to files under lib/lp/bugs/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4983.1.2 by Curtis Hovey
Added pylint exceptions to database classes.
4
# pylint: disable-msg=E0611,W0212
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
5
6
__metaclass__ = type
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
7
__all__ = [
8
    'BugTracker',
7675.837.20 by Bryce Harrington
Tidy up code
9
    'BugTrackerSet',
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
10
    'BugTrackerAlias',
11
    'BugTrackerAliasSet',
7675.837.2 by Bryce Harrington
Alphabetize class order
12
    'BugTrackerComponent',
13
    'BugTrackerComponentGroup',
11562.3.2 by Robert Collins
Move Active/Inactive batch navigators to batching for reuse and define a more useful api for getting sets of trackers.
14
    'BugTrackerSet',
7675.837.7 by Bryce Harrington
Implement routines for adding and getting component groups
15
    ]
7675.604.2 by Gavin Panella
Fix lint.
16
17
from datetime import datetime
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
18
from itertools import chain
5613.1.12 by Graham Binns
Fixed an assload of lint errors.
19
# splittype is not formally documented, but is in urllib.__all__, is
20
# simple, and is heavily used by the rest of urllib, hence is unlikely
21
# to change or go away.
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
22
from urllib import (
23
    quote,
24
    splittype,
25
    )
26
27
from lazr.uri import URI
28
from pytz import timezone
3018.2.1 by Stuart Bishop
Refactor celebrities to use a descriptor, removing need for boilerplate code. Also optimizes database access, ensuring at most one database query per celebrity per request.
29
from sqlobject import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
30
    BoolCol,
31
    ForeignKey,
32
    OR,
33
    SQLMultipleJoin,
34
    SQLObjectNotFound,
35
    StringCol,
36
    )
5796.13.14 by Gavin Panella
Show imported_bug_comments working on newly added data; order by BugMessage ASC not DESC.
37
from sqlobject.sqlbuilder import AND
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
38
from storm.expr import (
39
    Count,
40
    Desc,
41
    Not,
11132.4.21 by Graham Binns
Merged devel, resolved conflicts, instituted world peace.
42
    SQL,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
43
    )
12442.2.9 by j.c.sackett
Ran import reformatter per review.
44
from storm.locals import (
45
    Bool,
46
    Int,
47
    Reference,
48
    ReferenceSet,
49
    Unicode,
50
    )
7238.4.7 by Graham Binns
Updated BugTracker.getBugWatchesNeedingUpdate() and associated tests.
51
from storm.store import Store
12442.2.9 by j.c.sackett
Ran import reformatter per review.
52
from zope.component import getUtility
53
from zope.interface import implements
7238.4.7 by Graham Binns
Updated BugTracker.getBugWatchesNeedingUpdate() and associated tests.
54
12442.2.9 by j.c.sackett
Ran import reformatter per review.
55
from lp.app.errors import NotFoundError
13130.1.12 by Curtis Hovey
Sorted imports.
56
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
12442.2.2 by j.c.sackett
Moved validators to app, which makes more sense.
57
from lp.app.validators.email import valid_email
58
from lp.app.validators.name import sanitize_name
8687.4.1 by Graham Binns
Tidied up the lp.bugs.models.bugtracker imports.
59
from lp.bugs.interfaces.bugtracker import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
60
    BugTrackerType,
61
    IBugTracker,
62
    IBugTrackerAlias,
63
    IBugTrackerAliasSet,
7675.837.2 by Bryce Harrington
Alphabetize class order
64
    IBugTrackerComponent,
65
    IBugTrackerComponentGroup,
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
66
    IBugTrackerSet,
67
    SINGLE_PRODUCT_BUGTRACKERTYPES,
68
    )
69
from lp.bugs.interfaces.bugtrackerperson import BugTrackerPersonAlreadyExists
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
70
from lp.bugs.model.bug import Bug
71
from lp.bugs.model.bugmessage import BugMessage
8687.4.1 by Graham Binns
Tidied up the lp.bugs.models.bugtracker imports.
72
from lp.bugs.model.bugtrackerperson import BugTrackerPerson
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
73
from lp.bugs.model.bugwatch import BugWatch
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
74
from lp.registry.interfaces.person import (
75
    IPersonSet,
76
    validate_public_person,
77
    )
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
78
from lp.services.database.enumcol import EnumCol
79
from lp.services.database.lpstorm import IStore
80
from lp.services.database.sqlbase import (
81
    flush_database_updates,
82
    SQLBase,
83
    )
12243.4.3 by j.c.sackett
Moved lp.services.stormbase to lp.services.database.stormbase
84
from lp.services.database.stormbase import StormBase
14606.3.1 by William Grant
Merge canonical.database into lp.services.database.
85
from lp.services.helpers import shortlist
86
from lp.services.webapp.interfaces import (
87
    DEFAULT_FLAVOR,
88
    IStoreSelector,
89
    MAIN_STORE,
90
    )
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
91
92
93
def normalise_leading_slashes(rest):
94
    """Ensure that the 'rest' segment of a URL starts with //."""
6059.1.20 by Gavin Panella
Default title to baseurl when creating a bugtracker; Fix inconsistencies when searching for bugtrackers.
95
    return '//' + rest.lstrip('/')
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
96
97
98
def normalise_base_url(base_url):
99
    """Convert https to http, and normalise scheme for others."""
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
100
    schema, rest = splittype(base_url)
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
101
    if schema == 'https':
102
        return 'http:' + rest
103
    elif schema is None:
104
        return 'http:' + normalise_leading_slashes(base_url)
105
    else:
106
        return '%s:%s' % (schema, rest)
107
108
109
def base_url_permutations(base_url):
110
    """Return all the possible variants of a base URL.
111
112
    Sometimes the URL ends with slash, sometimes not. Sometimes http
113
    is used, sometimes https. This gives a list of all possible
114
    variants, so that queryByBaseURL can match a base URL, even if it
115
    doesn't match exactly what is stored in the database.
116
117
    >>> base_url_permutations('http://foo/bar')
118
    ['http://foo/bar', 'http://foo/bar/',
119
     'https://foo/bar', 'https://foo/bar/']
120
    """
121
    http_schemas = ['http', 'https']
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
122
    url_schema, rest = splittype(base_url)
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
123
    if url_schema in http_schemas or url_schema is None:
124
        possible_schemas = http_schemas
125
        rest = normalise_leading_slashes(rest)
126
    else:
127
        # This else-clause is here since we have no strict
128
        # requirement that bug trackers have to have http URLs.
129
        possible_schemas = [url_schema]
130
    alternative_urls = [base_url]
131
    for schema in possible_schemas:
132
        url = "%s:%s" % (schema, rest)
133
        if url != base_url:
134
            alternative_urls.append(url)
135
        if url.endswith('/'):
136
            alternative_urls.append(url[:-1])
137
        else:
138
            alternative_urls.append(url + '/')
139
    return alternative_urls
1716.1.90 by Christian Reis
Delintifying some of the database classes
140
6059.1.31 by Gavin Panella
Clean up some lint.
141
5613.1.20 by Graham Binns
Added a test for make_bugtracker_name(); also made make_bugtracker_name() less collision-y.
142
def make_bugtracker_name(uri):
143
    """Return a name string for a bug tracker based on a URI.
5613.1.2 by Graham Binns
databas.BugWatchSet.extractBugTrackerAndBug() now works for email addresses.
144
5613.1.20 by Graham Binns
Added a test for make_bugtracker_name(); also made make_bugtracker_name() less collision-y.
145
    :param uri: The base URI to be used to identify the bug tracker,
146
        e.g. http://bugs.example.com or mailto:bugs@example.com
5613.1.2 by Graham Binns
databas.BugWatchSet.extractBugTrackerAndBug() now works for email addresses.
147
    """
5613.1.20 by Graham Binns
Added a test for make_bugtracker_name(); also made make_bugtracker_name() less collision-y.
148
    base_uri = URI(uri)
149
    if base_uri.scheme == 'mailto':
150
        if valid_email(base_uri.path):
151
            base_name = base_uri.path.split('@', 1)[0]
5613.1.2 by Graham Binns
databas.BugWatchSet.extractBugTrackerAndBug() now works for email addresses.
152
        else:
6205.2.5 by Gavin Panella
Raise AssertionError instead of ValueError.
153
            raise AssertionError(
5613.1.20 by Graham Binns
Added a test for make_bugtracker_name(); also made make_bugtracker_name() less collision-y.
154
                'Not a valid email address: %s' % base_uri.path)
5613.1.2 by Graham Binns
databas.BugWatchSet.extractBugTrackerAndBug() now works for email addresses.
155
    else:
5613.1.20 by Graham Binns
Added a test for make_bugtracker_name(); also made make_bugtracker_name() less collision-y.
156
        base_name = base_uri.host
5613.1.2 by Graham Binns
databas.BugWatchSet.extractBugTrackerAndBug() now works for email addresses.
157
12599.4.2 by Leonard Richardson
Merge from trunk.
158
    return 'auto-%s' % sanitize_name(base_name)
5613.1.2 by Graham Binns
databas.BugWatchSet.extractBugTrackerAndBug() now works for email addresses.
159
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
160
6205.2.1 by Gavin Panella
New function make_bugtracker_title.
161
def make_bugtracker_title(uri):
162
    """Return a title string for a bug tracker based on a URI.
163
164
    :param uri: The base URI to be used to identify the bug tracker,
165
        e.g. http://bugs.example.com or mailto:bugs@example.com
166
    """
167
    base_uri = URI(uri)
168
    if base_uri.scheme == 'mailto':
169
        if valid_email(base_uri.path):
170
            local_part, domain = base_uri.path.split('@', 1)
171
            domain_parts = domain.split('.')
172
            return 'Email to %s@%s' % (local_part, domain_parts[0])
173
        else:
6205.2.5 by Gavin Panella
Raise AssertionError instead of ValueError.
174
            raise AssertionError(
6205.2.1 by Gavin Panella
New function make_bugtracker_title.
175
                'Not a valid email address: %s' % base_uri.path)
176
    else:
6205.2.2 by Gavin Panella
Include the path in the bugtracker title.
177
        return base_uri.host + base_uri.path
6205.2.1 by Gavin Panella
New function make_bugtracker_title.
178
179
12243.4.2 by j.c.sackett
Updated all uses of storm.base.Storm with lp.services.stormbase.StormBase
180
class BugTrackerComponent(StormBase):
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
181
    """The software component in the remote bug tracker.
182
183
    Most bug trackers organize bug reports by the software 'component'
184
    they affect.  This class provides a mapping of this upstream component
185
    to the corresponding source package in the distro.
186
    """
187
    implements(IBugTrackerComponent)
188
    __storm_table__ = 'BugTrackerComponent'
189
190
    id = Int(primary=True)
191
    name = Unicode(allow_none=False)
192
193
    component_group_id = Int('component_group')
194
    component_group = Reference(
195
        component_group_id,
196
        'BugTrackerComponentGroup.id')
197
198
    is_visible = Bool(allow_none=False)
199
    is_custom = Bool(allow_none=False)
200
201
    distribution_id = Int('distribution')
202
    distribution = Reference(
203
        distribution_id,
204
        'Distribution.id')
205
206
    source_package_name_id = Int('source_package_name')
207
    source_package_name = Reference(
208
        source_package_name_id,
209
        'SourcePackageName.id')
210
211
    def _get_distro_source_package(self):
212
        """Retrieves the corresponding source package"""
213
        if self.distribution is None or self.source_package_name is None:
214
            return None
215
        return self.distribution.getSourcePackage(
216
            self.source_package_name)
217
218
    def _set_distro_source_package(self, dsp):
219
        """Links this component to its corresponding source package"""
220
        if dsp is None:
221
            self.distribution = None
222
            self.source_package_name = None
223
        else:
224
            self.distribution = dsp.distribution
7675.891.35 by Bryce Harrington
Re-merge API branch this branch depends on.
225
            self.source_package_name = dsp.sourcepackagename
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
226
7675.891.35 by Bryce Harrington
Re-merge API branch this branch depends on.
227
    distro_source_package = property(
228
        _get_distro_source_package,
229
        _set_distro_source_package,
230
        None,
231
        """The distribution's source package for this component""")
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
232
233
12243.4.2 by j.c.sackett
Updated all uses of storm.base.Storm with lp.services.stormbase.StormBase
234
class BugTrackerComponentGroup(StormBase):
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
235
    """A collection of components in a remote bug tracker.
236
237
    Some bug trackers organize sets of components into higher level
238
    groups, such as Bugzilla's 'product'.
239
    """
240
    implements(IBugTrackerComponentGroup)
241
    __storm_table__ = 'BugTrackerComponentGroup'
242
243
    id = Int(primary=True)
244
    name = Unicode(allow_none=False)
245
    bug_tracker_id = Int('bug_tracker')
246
    bug_tracker = Reference(bug_tracker_id, 'BugTracker.id')
247
    components = ReferenceSet(
248
        id,
249
        BugTrackerComponent.component_group_id,
250
        order_by=BugTrackerComponent.name)
251
252
    def addComponent(self, component_name):
253
        """Adds a component that is synced from a remote bug tracker"""
254
255
        component = BugTrackerComponent()
256
        component.name = component_name
257
        component.component_group = self
258
259
        store = IStore(BugTrackerComponent)
260
        store.add(component)
261
        store.flush()
262
263
        return component
264
265
    def getComponent(self, component_name):
13210.2.2 by Bryce Harrington
Update docstring. Either name or id number is permitted.
266
        """Retrieves a component by the given name or id number.
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
267
268
        None is returned if there is no component by that name in the
269
        group.
270
        """
271
272
        if component_name is None:
273
            return None
7675.1188.12 by Bryce Harrington
Switch from using the component name in the path to using id's.
274
        elif component_name.isdigit():
275
            component_id = int(component_name)
276
            return Store.of(self).find(
277
                BugTrackerComponent,
278
                BugTrackerComponent.id == component_id,
279
                BugTrackerComponent.component_group == self.id).one()
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
280
        else:
281
            return Store.of(self).find(
282
                BugTrackerComponent,
7675.1188.11 by Bryce Harrington
Re-merge db-devel trunk
283
                BugTrackerComponent.name == component_name,
284
                BugTrackerComponent.component_group == self.id).one()
7675.891.31 by Bryce Harrington
Move BugTrackerComponent back up to top of file.
285
286
    def addCustomComponent(self, component_name):
287
        """Adds a component locally that isn't synced from a remote tracker
288
        """
289
290
        component = BugTrackerComponent()
291
        component.name = component_name
292
        component.component_group = self
293
        component.is_custom = True
294
295
        store = IStore(BugTrackerComponent)
296
        store.add(component)
297
        store.flush()
298
299
        return component
300
301
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
302
class BugTracker(SQLBase):
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
303
    """A class to access the BugTracker table in the database.
304
305
    Each BugTracker is a distinct instance of that bug tracking
306
    tool. For example, each Bugzilla deployment is a separate
307
    BugTracker. bugzilla.mozilla.org and bugzilla.gnome.org are each
308
    distinct BugTrackers.
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
309
    """
310
    implements(IBugTracker)
1716.3.31 by kiko
Order the bugtracker listing, by title, so the end-user is not confused
311
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
312
    _table = 'BugTracker'
1716.3.31 by kiko
Order the bugtracker listing, by title, so the end-user is not confused
313
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
314
    bugtrackertype = EnumCol(dbName='bugtrackertype',
315
        schema=BugTrackerType, notNull=True)
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
316
    name = StringCol(notNull=True, unique=True)
317
    title = StringCol(notNull=True)
5852.2.2 by James Henstridge
fix some columns that claimed to be notNull=True but are in fact not not null
318
    summary = StringCol(notNull=False)
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
319
    baseurl = StringCol(notNull=True)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
320
    active = Bool(
321
        name='active', allow_none=False, default=True)
322
5485.1.17 by Edwin Grubbs
Fixed indentation
323
    owner = ForeignKey(
324
        dbName='owner', foreignKey='Person',
5821.2.40 by James Henstridge
* Move all the uses of public_person_validator over to the Storm
325
        storm_validator=validate_public_person, notNull=True)
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
326
    contactdetails = StringCol(notNull=False)
7147.1.5 by Graham Binns
Added a has_lp_plugin field to IBugTracker.
327
    has_lp_plugin = BoolCol(notNull=False, default=False)
3691.139.3 by Bjorn Tillenius
a Project should have only one bug tracker.
328
    projects = SQLMultipleJoin(
10724.1.6 by Henning Eggers
Vocabulary names.
329
        'ProjectGroup', joinColumn='bugtracker', orderBy='name')
5005.2.2 by Gavin Panella
Implementation.
330
    products = SQLMultipleJoin(
331
        'Product', joinColumn='bugtracker', orderBy='name')
8687.4.1 by Graham Binns
Tidied up the lp.bugs.models.bugtracker imports.
332
    watches = SQLMultipleJoin(
333
        'BugWatch', joinColumn='bugtracker', orderBy='-datecreated',
334
        prejoins=['bug'])
7675.837.8 by Bryce Harrington
Add factory methods for creating component/component_group tests
335
7634.3.19 by Graham Binns
Review changes for Danilo.
336
    _filing_url_patterns = {
337
        BugTrackerType.BUGZILLA: (
7849.7.3 by Graham Binns
Added trackers for SourceForge, RT and Bugzilla.
338
            "%(base_url)s/enter_bug.cgi?product=%(remote_product)s"
339
            "&short_desc=%(summary)s&long_desc=%(description)s"),
8342.4.1 by Graham Binns
Merged Google Code watches stuff back into the branch.
340
        BugTrackerType.GOOGLE_CODE: (
10007.1.1 by luke at faraone
Remove literal "&" from BugTrackerType.GOOGLE_CODE, replace with "&".
341
            "%(base_url)s/entry?summary=%(summary)s&"
8342.4.1 by Graham Binns
Merged Google Code watches stuff back into the branch.
342
            "comment=%(description)s"),
7849.7.7 by Graham Binns
Added nonsense for Mantis.
343
        BugTrackerType.MANTIS: (
344
            "%(base_url)s/bug_report_advanced_page.php"
345
            "?summary=%(summary)s&description=%(description)s"),
7849.7.5 by Graham Binns
Added search links for Roundup.
346
        BugTrackerType.PHPPROJECT: (
347
            "%(base_url)s/report.php"
348
            "?in[sdesc]=%(summary)s&in[ldesc]=%(description)s"),
7849.7.6 by Graham Binns
Added bug filing links for Roundup.
349
        BugTrackerType.ROUNDUP: (
350
            "%(base_url)s/issue?@template=item&title=%(summary)s"
351
            "&@note=%(description)s"),
7634.3.19 by Graham Binns
Review changes for Danilo.
352
        BugTrackerType.RT: (
7849.7.3 by Graham Binns
Added trackers for SourceForge, RT and Bugzilla.
353
            "%(base_url)s/Ticket/Create.html?Queue=%(remote_product)s"
354
            "&Subject=%(summary)s&Content=%(description)s"),
7634.3.19 by Graham Binns
Review changes for Danilo.
355
        BugTrackerType.SAVANE: (
356
            "%(base_url)s/bugs/?func=additem&group=%(remote_product)s"),
357
        BugTrackerType.SOURCEFORGE: (
358
            "%(base_url)s/%(tracker)s/?func=add&"
7849.7.5 by Graham Binns
Added search links for Roundup.
359
            "group_id=%(group_id)s&atid=%(at_id)s"),
11807.5.2 by Deryck Hodge
Fix the filing url patterns for Trac, to get test passing
360
        BugTrackerType.TRAC: (
361
            "%(base_url)s/newticket?summary=%(summary)s&"
362
            "description=%(description)s"),
7634.3.19 by Graham Binns
Review changes for Danilo.
363
        }
364
365
    _search_url_patterns = {
366
        BugTrackerType.BUGZILLA: (
7849.7.3 by Graham Binns
Added trackers for SourceForge, RT and Bugzilla.
367
            "%(base_url)s/query.cgi?product=%(remote_product)s"
368
            "&short_desc=%(summary)s"),
8342.4.1 by Graham Binns
Merged Google Code watches stuff back into the branch.
369
        BugTrackerType.GOOGLE_CODE: "%(base_url)s/list?q=%(summary)s",
7767.1.1 by Graham Binns
Added a search URL for debbugs.
370
        BugTrackerType.DEBBUGS: (
7849.7.8 by Graham Binns
Finished adding summary and description for upstream bugtracker links.
371
            "%(base_url)s/cgi-bin/search.cgi?phrase=%(summary)s"
372
            "&attribute_field=package&attribute_operator=STROREQ"
373
            "&attribute_value=%(remote_product)s"),
7634.3.19 by Graham Binns
Review changes for Danilo.
374
        BugTrackerType.MANTIS: "%(base_url)s/view_all_bug_page.php",
7849.7.5 by Graham Binns
Added search links for Roundup.
375
        BugTrackerType.PHPPROJECT: (
376
            "%(base_url)s/search.php?search_for=%(summary)s"),
377
        BugTrackerType.ROUNDUP: (
378
            "%(base_url)s/issue?@template=search&@search_text=%(summary)s"),
7634.3.19 by Graham Binns
Review changes for Danilo.
379
        BugTrackerType.RT: (
7634.3.10 by Graham Binns
Added tests for search URLS.
380
            "%(base_url)s/Search/Build.html?Query=Queue = "
7849.7.3 by Graham Binns
Added trackers for SourceForge, RT and Bugzilla.
381
            "'%(remote_product)s' AND Subject LIKE '%(summary)s'"),
7344.3.3 by Graham Binns
Added core functionality.
382
        BugTrackerType.SAVANE: (
7634.3.19 by Graham Binns
Review changes for Danilo.
383
            "%(base_url)s/bugs/?func=search&group=%(remote_product)s"),
7344.3.3 by Graham Binns
Added core functionality.
384
        BugTrackerType.SOURCEFORGE: (
7849.7.3 by Graham Binns
Added trackers for SourceForge, RT and Bugzilla.
385
            "%(base_url)s/search/?group_id=%(group_id)s"
386
            "&some_word=%(summary)s&type_of_search=artifact"),
7849.7.5 by Graham Binns
Added search links for Roundup.
387
        BugTrackerType.TRAC: "%(base_url)s/search?ticket=on&q=%(summary)s",
7344.3.3 by Graham Binns
Added core functionality.
388
        }
389
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
390
    @property
8687.4.2 by Graham Binns
The gnome-bugs bugtracker now uses a custom URL for its bug filing link.
391
    def _custom_filing_url_patterns(self):
392
        """Return a dict of bugtracker-specific bugfiling URL patterns."""
393
        gnome_bugzilla = getUtility(ILaunchpadCelebrities).gnome_bugzilla
394
        return {
395
            gnome_bugzilla: (
396
                "%(base_url)s/enter_bug.cgi?product=%(remote_product)s"
397
                "&short_desc=%(summary)s&comment=%(description)s"),
398
            }
399
400
    @property
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
401
    def latestwatches(self):
7344.3.7 by Graham Binns
Cleanup for Brad and Paul.
402
        """See `IBugTracker`."""
3024.1.35 by Christian Reis
Implement performance fixes on the top timeout pages, mainly through the use of prejoins, properties and one cachedproperty: bugtracker-index, person-packages, distribution-allpackages, distroreleaselanguage, products-all, products-index, bug listings and a few others. This should make even soft timeouts on those pages hopefully disappear.
403
        return self.watches[:10]
404
7634.3.2 by Graham Binns
Added BugTracker.requires_remote_product and tests.
405
    @property
7634.3.5 by Graham Binns
Renamed BugTracker.requires_remote_product -> multi_product per allenap's ideas. Corrected docstrings.
406
    def multi_product(self):
7634.3.7 by Graham Binns
Review changes.
407
        """Return True if this BugTracker tracks multiple projects."""
7634.3.5 by Graham Binns
Renamed BugTracker.requires_remote_product -> multi_product per allenap's ideas. Corrected docstrings.
408
        if self.bugtrackertype not in SINGLE_PRODUCT_BUGTRACKERTYPES:
7634.3.2 by Graham Binns
Added BugTracker.requires_remote_product and tests.
409
            return True
410
        else:
411
            return False
412
7849.7.1 by Graham Binns
Added summary and description parameters to getBugFilingAndSearchLinks().
413
    def getBugFilingAndSearchLinks(self, remote_product, summary=None,
13405.8.1 by Bryce Harrington
Add optional remote_component parameter to getBugFilingAndSearchLinks
414
                                   description=None, remote_component=None):
7344.3.2 by Graham Binns
Added tests and basics.
415
        """See `IBugTracker`."""
7634.3.19 by Graham Binns
Review changes for Danilo.
416
        bugtracker_urls = {'bug_filing_url': None, 'bug_search_url': None}
7634.3.9 by Graham Binns
BugTracker.getBugFilingLink() -> BugTracker->getBugFilingAndSearchLinks().
417
7634.3.5 by Graham Binns
Renamed BugTracker.requires_remote_product -> multi_product per allenap's ideas. Corrected docstrings.
418
        if remote_product is None and self.multi_product:
7634.3.4 by Graham Binns
Added a test for bugtrackers that don't need a remote product to product.txt.
419
            # Don't try to return anything if remote_product is required
420
            # for this BugTrackerType and one hasn't been passed.
7767.1.5 by Graham Binns
Removed the (now unnecessary) test fixes that I had to put in when trunk broke.
421
            return bugtracker_urls
7634.3.7 by Graham Binns
Review changes.
422
423
        if remote_product is None:
7634.3.2 by Graham Binns
Added BugTracker.requires_remote_product and tests.
424
            # Turn the remote product into an empty string so that
425
            # quote() doesn't blow up later on.
426
            remote_product = ''
427
13405.8.2 by Bryce Harrington
Add remote_component to url_components
428
        if remote_component is None:
429
            # Ditto for remote component.
430
            remote_component = ''
431
8687.4.2 by Graham Binns
The gnome-bugs bugtracker now uses a custom URL for its bug filing link.
432
        if self in self._custom_filing_url_patterns:
433
            # Some bugtrackers are customised to accept different
434
            # querystring parameters from the default. We special-case
435
            # these.
436
            bug_filing_pattern = self._custom_filing_url_patterns[self]
437
        else:
438
            bug_filing_pattern = self._filing_url_patterns.get(
439
                self.bugtrackertype, None)
440
7634.3.19 by Graham Binns
Review changes for Danilo.
441
        bug_search_pattern = self._search_url_patterns.get(
442
            self.bugtrackertype, None)
7344.3.7 by Graham Binns
Cleanup for Brad and Paul.
443
7344.3.3 by Graham Binns
Added core functionality.
444
        # Make sure that we don't put > 1 '/' in returned URLs.
445
        base_url = self.baseurl.rstrip('/')
446
7849.7.1 by Graham Binns
Added summary and description parameters to getBugFilingAndSearchLinks().
447
        # If summary or description are None, convert them to empty
448
        # strings to that we don't try to pass anything to the upstream
449
        # bug tracker.
450
        if summary is None:
451
            summary = ''
452
        if description is None:
453
            description = ''
454
8132.2.1 by Graham Binns
BugTracker.getBugFilingAndSearchLinks() now handles unicode properly.
455
        # UTF-8 encode the description and summary so that quote()
456
        # doesn't break if they contain unicode characters it doesn't
457
        # understand.
458
        summary = summary.encode('utf-8')
459
        description = description.encode('utf-8')
460
7344.3.8 by Graham Binns
Fixed a silly coding snafu.
461
        if self.bugtrackertype == BugTrackerType.SOURCEFORGE:
13128.1.1 by Graham Binns
Added test and fix.
462
            try:
463
                # SourceForge bug trackers use a group ID and an ATID to
464
                # file a bug, rather than a product name. remote_product
465
                # should be an ampersand-separated string in the form
466
                # 'group_id&atid'
467
                group_id, at_id = remote_product.split('&')
468
            except ValueError:
469
                # If remote_product contains something that's not valid
470
                # in a SourceForge context we just return early.
471
                return None
7344.3.3 by Graham Binns
Added core functionality.
472
473
            # If this bug tracker is the SourceForge celebrity the link
474
            # is to the new bug tracker rather than the old one.
475
            sf_celeb = getUtility(ILaunchpadCelebrities).sourceforge_tracker
476
            if self == sf_celeb:
477
                tracker = 'tracker2'
478
            else:
479
                tracker = 'tracker'
480
7634.3.10 by Graham Binns
Added tests for search URLS.
481
            url_components = {
7344.3.3 by Graham Binns
Added core functionality.
482
                'base_url': base_url,
7344.3.9 by Graham Binns
Fix and tests for bug 304849
483
                'tracker': quote(tracker),
484
                'group_id': quote(group_id),
485
                'at_id': quote(at_id),
7849.7.8 by Graham Binns
Finished adding summary and description for upstream bugtracker links.
486
                'summary': quote(summary),
487
                'description': quote(description),
7634.3.10 by Graham Binns
Added tests for search URLS.
488
                }
7344.3.3 by Graham Binns
Added core functionality.
489
7344.3.5 by Graham Binns
Documentation and a minor code tweak.
490
        else:
7634.3.10 by Graham Binns
Added tests for search URLS.
491
            url_components = {
7344.3.5 by Graham Binns
Documentation and a minor code tweak.
492
                'base_url': base_url,
7344.3.9 by Graham Binns
Fix and tests for bug 304849
493
                'remote_product': quote(remote_product),
13405.8.2 by Bryce Harrington
Add remote_component to url_components
494
                'remote_component': quote(remote_component),
7849.7.8 by Graham Binns
Finished adding summary and description for upstream bugtracker links.
495
                'summary': quote(summary),
496
                'description': quote(description),
7634.3.10 by Graham Binns
Added tests for search URLS.
497
                }
498
7634.3.19 by Graham Binns
Review changes for Danilo.
499
        if bug_filing_pattern is not None:
500
            bugtracker_urls['bug_filing_url'] = (
501
                bug_filing_pattern % url_components)
502
        if bug_search_pattern is not None:
503
            bugtracker_urls['bug_search_url'] = (
504
                bug_search_pattern % url_components)
7344.3.2 by Graham Binns
Added tests and basics.
505
7634.3.9 by Graham Binns
BugTracker.getBugFilingLink() -> BugTracker->getBugFilingAndSearchLinks().
506
        return bugtracker_urls
507
2950.2.1 by James Henstridge
Make /malone/bugtrackers/$bugtrackername/$remotebug redirect to the
508
    def getBugsWatching(self, remotebug):
7344.3.2 by Graham Binns
Added tests and basics.
509
        """See `IBugTracker`."""
5613.1.4 by Graham Binns
BugTracker.getBugsWathching() now always returns [] when called on an email address bug tracker.
510
        # We special-case email address bug trackers. Since we don't
511
        # record a remote bug id for them we can never know which bugs
512
        # are already watching a remote bug.
513
        if self.bugtrackertype == BugTrackerType.EMAILADDRESS:
514
            return []
515
2950.2.1 by James Henstridge
Make /malone/bugtrackers/$bugtrackername/$remotebug redirect to the
516
        return shortlist(Bug.select(AND(BugWatch.q.bugID == Bug.q.id,
517
                                        BugWatch.q.bugtrackerID == self.id,
518
                                        BugWatch.q.remotebug == remotebug),
519
                                    distinct=True,
520
                                    orderBy=['datecreated']))
521
7675.604.7 by Gavin Panella
Add new properties to BugTracker: watches_ready_to_check, watches_with_unpushed_comments and watches_needing_update.
522
    @property
523
    def watches_ready_to_check(self):
524
        return Store.of(self).find(
7238.4.7 by Graham Binns
Updated BugTracker.getBugWatchesNeedingUpdate() and associated tests.
525
            BugWatch,
7337.7.3 by Graham Binns
Review changes for Gavin.
526
            BugWatch.bugtracker == self,
7675.595.6 by Graham Binns
Updated tests to take account of the lastcheck -> next_check transition.
527
            Not(BugWatch.next_check == None),
528
            BugWatch.next_check <= datetime.now(timezone('UTC')))
7238.4.7 by Graham Binns
Updated BugTracker.getBugWatchesNeedingUpdate() and associated tests.
529
7675.604.7 by Gavin Panella
Add new properties to BugTracker: watches_ready_to_check, watches_with_unpushed_comments and watches_needing_update.
530
    @property
531
    def watches_with_unpushed_comments(self):
532
        return Store.of(self).find(
7238.4.7 by Graham Binns
Updated BugTracker.getBugWatchesNeedingUpdate() and associated tests.
533
            BugWatch,
7337.7.3 by Graham Binns
Review changes for Gavin.
534
            BugWatch.bugtracker == self,
7238.4.7 by Graham Binns
Updated BugTracker.getBugWatchesNeedingUpdate() and associated tests.
535
            BugMessage.bugwatch == BugWatch.id,
7675.604.7 by Gavin Panella
Add new properties to BugTracker: watches_ready_to_check, watches_with_unpushed_comments and watches_needing_update.
536
            BugMessage.remote_comment_id == None).config(distinct=True)
537
538
    @property
539
    def watches_needing_update(self):
540
        """All watches needing some sort of update.
541
542
        :return: The union of `watches_ready_to_check` and
543
            `watches_with_unpushed_comments`.
544
        """
545
        return self.watches_ready_to_check.union(
546
            self.watches_with_unpushed_comments)
547
5308.1.22 by Gavin Panella
Small changes as suggested by Barry in review.
548
    # Join to return a list of BugTrackerAliases relating to this
549
    # BugTracker.
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
550
    _bugtracker_aliases = SQLMultipleJoin(
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
551
        'BugTrackerAlias', joinColumn='bugtracker')
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
552
553
    def _get_aliases(self):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
554
        """See `IBugTracker.aliases`."""
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
555
        alias_urls = set(alias.base_url for alias in self._bugtracker_aliases)
556
        # Although it does no harm if the current baseurl is also an
5906.3.5 by Gavin Panella
Demonstrate that minor changes to the main location of a bug tracker can be made without going insane.
557
        # alias, we hide it and all its permutations to avoid
558
        # confusion.
559
        alias_urls.difference_update(base_url_permutations(self.baseurl))
5308.1.23 by Gavin Panella
Make bugtracker.aliases returns a tuple.
560
        return tuple(sorted(alias_urls))
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
561
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
562
    def _set_aliases(self, alias_urls):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
563
        """See `IBugTracker.aliases`."""
5308.1.9 by Gavin Panella
Redefine deletion of aliases as setting to an empty list so that moving parts are in one place.
564
        if alias_urls is None:
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
565
            alias_urls = set()
566
        else:
567
            alias_urls = set(alias_urls)
5308.1.9 by Gavin Panella
Redefine deletion of aliases as setting to an empty list so that moving parts are in one place.
568
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
569
        current_aliases_by_url = dict(
570
            (alias.base_url, alias) for alias in self._bugtracker_aliases)
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
571
        # Make a set of the keys, i.e. a set of current URLs.
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
572
        current_alias_urls = set(current_aliases_by_url)
573
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
574
        # URLs we need to add as aliases.
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
575
        to_add = alias_urls - current_alias_urls
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
576
        # URL aliases we need to delete.
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
577
        to_del = current_alias_urls - alias_urls
578
579
        for url in to_add:
580
            BugTrackerAlias(bugtracker=self, base_url=url)
581
        for url in to_del:
582
            alias = current_aliases_by_url[url]
583
            alias.destroySelf()
584
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
585
    aliases = property(
586
        _get_aliases, _set_aliases, None,
587
        """A list of the alias URLs. See `IBugTracker`.
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
588
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
589
        The aliases are found by querying BugTrackerAlias. Assign an
590
        iterable of URLs or None to set or remove aliases.
591
        """)
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
592
5796.13.1 by Gavin Panella
Implementation of deleting bug trackers.
593
    @property
5796.13.4 by Gavin Panella
Don't query for Messages, just get BugMessages.
594
    def imported_bug_messages(self):
5796.13.1 by Gavin Panella
Implementation of deleting bug trackers.
595
        """See `IBugTracker`."""
5796.13.4 by Gavin Panella
Don't query for Messages, just get BugMessages.
596
        return BugMessage.select(
597
            AND((BugMessage.q.bugwatchID == BugWatch.q.id),
5796.13.5 by Gavin Panella
Documentation for imported_bug_messages; sort the bug messages too.
598
                (BugWatch.q.bugtrackerID == self.id)),
5796.13.14 by Gavin Panella
Show imported_bug_comments working on newly added data; order by BugMessage ASC not DESC.
599
            orderBy=BugMessage.q.id)
5796.13.1 by Gavin Panella
Implementation of deleting bug trackers.
600
6331.3.12 by Graham Binns
Moved BugTrackerPersonSet logic into BugTracker.
601
    def getLinkedPersonByName(self, name):
602
        """Return the Person with a given name on this bugtracker."""
603
        return BugTrackerPerson.selectOneBy(name=name, bugtracker=self)
604
605
    def linkPersonToSelf(self, name, person):
606
        """See `IBugTrackerSet`."""
607
        # Check that this name isn't already in use for this bugtracker.
608
        if self.getLinkedPersonByName(name) is not None:
609
            raise BugTrackerPersonAlreadyExists(
610
                "Name '%s' is already in use for bugtracker '%s'." %
611
                (name, self.name))
612
613
        bugtracker_person = BugTrackerPerson(
614
            name=name, bugtracker=self, person=person)
615
616
        return bugtracker_person
617
6325.2.38 by Graham Binns
Moved ensurePerson... into BugTracker.
618
    def ensurePersonForSelf(
6325.2.37 by Graham Binns
Merged bugtracker person branch.
619
        self, display_name, email, rationale, creation_comment):
620
        """Return a Person that is linked to this bug tracker."""
621
        # If we have an email address to work with we can use
622
        # ensurePerson() to get the Person we need.
623
        if email is not None:
624
            return getUtility(IPersonSet).ensurePerson(
625
                email, display_name, rationale, creation_comment)
626
627
        # First, see if there's already a BugTrackerPerson for this
628
        # display_name on this bugtracker. If there is, return it.
629
        bugtracker_person = self.getLinkedPersonByName(display_name)
630
631
        if bugtracker_person is not None:
632
            return bugtracker_person.person
633
634
        # Generate a valid Launchpad name for the Person.
635
        base_canonical_name = (
6325.2.38 by Graham Binns
Moved ensurePerson... into BugTracker.
636
            "%s-%s" % (sanitize_name(display_name), self.name))
6325.2.37 by Graham Binns
Merged bugtracker person branch.
637
        canonical_name = base_canonical_name
638
639
        person_set = getUtility(IPersonSet)
640
        index = 0
641
        while person_set.getByName(canonical_name) is not None:
642
            index += 1
643
            canonical_name = "%s-%s" % (base_canonical_name, index)
644
645
        person = person_set.createPersonWithoutEmail(
646
            canonical_name, rationale, creation_comment,
647
            displayname=display_name)
648
649
        # Link the Person to the bugtracker for future reference.
650
        bugtracker_person = self.linkPersonToSelf(display_name, person)
651
652
        return person
653
11132.4.22 by Graham Binns
Added an AuthorizationBase class for Bugtracker so that we don't have to futz around with permissions.
654
    def resetWatches(self, new_next_check=None):
9600.2.4 by Graham Binns
Added resetWatches() method to IBugTracker.
655
        """See `IBugTracker`."""
11132.4.1 by Graham Binns
Added a test for the new randomisation code. Expressed in Storm, it breaks.
656
        if new_next_check is None:
657
            new_next_check = SQL(
11132.4.11 by Graham Binns
Changed 7 days to 1; 7 just causes problems.
658
                "now() at time zone 'UTC' + (random() * interval '1 day')")
7675.595.6 by Graham Binns
Updated tests to take account of the lastcheck -> next_check transition.
659
9600.2.4 by Graham Binns
Added resetWatches() method to IBugTracker.
660
        store = Store.of(self)
11132.4.2 by Graham Binns
resetWatches() now randomises the next_check times for all bug watches unless told not to.
661
        store.find(BugWatch, BugWatch.bugtracker == self).set(
662
            next_check=new_next_check, lastchecked=None,
663
            last_error_type=None)
9600.2.4 by Graham Binns
Added resetWatches() method to IBugTracker.
664
7675.837.7 by Bryce Harrington
Implement routines for adding and getting component groups
665
    def addRemoteComponentGroup(self, component_group_name):
666
        """See `IBugTracker`."""
7675.837.21 by Bryce Harrington
Fix lintian issues
667
7675.837.15 by Bryce Harrington
Enable storing data into the database now that permissions are there.
668
        if component_group_name is None:
669
            component_group_name = "default"
670
        component_group = BugTrackerComponentGroup()
671
        component_group.name = component_group_name
7675.837.12 by Bryce Harrington
Persist components and component_groups
672
        component_group.bug_tracker = self
7675.837.11 by Bryce Harrington
Hook up component groups to their bug trackers
673
7675.837.12 by Bryce Harrington
Persist components and component_groups
674
        store = IStore(BugTrackerComponentGroup)
675
        store.add(component_group)
7675.837.15 by Bryce Harrington
Enable storing data into the database now that permissions are there.
676
        store.commit()
7675.837.11 by Bryce Harrington
Hook up component groups to their bug trackers
677
7675.837.8 by Bryce Harrington
Add factory methods for creating component/component_group tests
678
        return component_group
7675.837.7 by Bryce Harrington
Implement routines for adding and getting component groups
679
7675.837.40 by Bryce Harrington
Restore getAllRemoteComponentGroups()
680
    def getAllRemoteComponentGroups(self):
681
        """See `IBugTracker`."""
682
        component_groups = []
683
684
        component_groups = Store.of(self).find(
685
            BugTrackerComponentGroup,
686
            BugTrackerComponentGroup.bug_tracker == self.id)
687
        component_groups = component_groups.order_by(
688
            BugTrackerComponentGroup.name)
689
        return component_groups
690
7675.837.11 by Bryce Harrington
Hook up component groups to their bug trackers
691
    def getRemoteComponentGroup(self, component_group_name):
7675.837.7 by Bryce Harrington
Implement routines for adding and getting component groups
692
        """See `IBugTracker`."""
7675.837.13 by Bryce Harrington
Attempt to get getAllRemoteComponentGroups() hooked up, but something
693
        component_group = None
7675.837.15 by Bryce Harrington
Enable storing data into the database now that permissions are there.
694
        store = IStore(BugTrackerComponentGroup)
13210.1.1 by Bryce Harrington
Verify component_group_name isn't None before dereferencing
695
        if component_group_name is None:
696
            return None
697
        elif component_group_name.isdigit():
7675.1188.12 by Bryce Harrington
Switch from using the component name in the path to using id's.
698
            component_group_id = int(component_group_name)
699
            component_group = store.find(
700
                BugTrackerComponentGroup,
13155.1.13 by Curtis Hovey
Use find().one() because the db has a sane constraint.
701
                BugTrackerComponentGroup.id == component_group_id).one()
7675.1188.12 by Bryce Harrington
Switch from using the component name in the path to using id's.
702
        else:
703
            component_group = store.find(
704
                BugTrackerComponentGroup,
13155.1.13 by Curtis Hovey
Use find().one() because the db has a sane constraint.
705
                BugTrackerComponentGroup.name == component_group_name).one()
7675.837.13 by Bryce Harrington
Attempt to get getAllRemoteComponentGroups() hooked up, but something
706
        return component_group
7675.837.7 by Bryce Harrington
Implement routines for adding and getting component groups
707
13155.1.19 by Curtis Hovey
Rename getRemoteComponentForDistroSourcePackage to getRemoteComponentForDistroSourcePackageName
708
    def getRemoteComponentForDistroSourcePackageName(
709
        self, distribution, sourcepackagename):
7675.1188.26 by Bryce Harrington
Review by thumper: Move check for existing component links out of
710
        """See `IBugTracker`."""
7675.1188.30 by Bryce Harrington
Check for (and fix) bad distro name
711
        if distribution is None:
712
            return None
13155.1.19 by Curtis Hovey
Rename getRemoteComponentForDistroSourcePackage to getRemoteComponentForDistroSourcePackageName
713
        dsp = distribution.getSourcePackage(sourcepackagename)
13155.1.13 by Curtis Hovey
Use find().one() because the db has a sane constraint.
714
        if dsp is None:
715
            return None
716
        return Store.of(self).find(
7675.1188.26 by Bryce Harrington
Review by thumper: Move check for existing component links out of
717
            BugTrackerComponent,
718
            BugTrackerComponent.distribution == distribution.id,
719
            BugTrackerComponent.source_package_name ==
13155.1.13 by Curtis Hovey
Use find().one() because the db has a sane constraint.
720
                dsp.sourcepackagename.id).one()
7675.1188.26 by Bryce Harrington
Review by thumper: Move check for existing component links out of
721
6325.2.37 by Graham Binns
Merged bugtracker person branch.
722
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
723
class BugTrackerSet:
11132.4.2 by Graham Binns
resetWatches() now randomises the next_check times for all bug watches unless told not to.
724
    """Implements IBugTrackerSet for a container or set of BugTrackers,
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
725
    either the full set in the db, or a subset.
726
    """
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
727
728
    implements(IBugTrackerSet)
729
730
    table = BugTracker
1382 by Canonical.com Patch Queue Manager
new page layout
731
732
    def __init__(self):
5512.2.9 by Matthew Paul Thomas
Applies the same love to the 'Register an external bug tracker' page.
733
        self.title = 'Bug trackers registered in Launchpad'
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
734
3018.2.1 by Stuart Bishop
Refactor celebrities to use a descriptor, removing need for boilerplate code. Also optimizes database access, ensuring at most one database query per celebrity per request.
735
    def get(self, bugtracker_id, default=None):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
736
        """See `IBugTrackerSet`."""
3018.2.1 by Stuart Bishop
Refactor celebrities to use a descriptor, removing need for boilerplate code. Also optimizes database access, ensuring at most one database query per celebrity per request.
737
        try:
738
            return BugTracker.get(bugtracker_id)
739
        except SQLObjectNotFound:
740
            return default
741
3018.2.2 by Stuart Bishop
Add BugTrackerSet.getByName
742
    def getByName(self, name, default=None):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
743
        """See `IBugTrackerSet`."""
3018.2.2 by Stuart Bishop
Add BugTrackerSet.getByName
744
        return self.table.selectOne(self.table.q.name == name)
745
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
746
    def __getitem__(self, name):
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
747
        item = self.table.selectOne(self.table.q.name == name)
748
        if item is None:
2628 by Canonical.com Patch Queue Manager
[trivial] converted a bunch of browser:traverse into navigation
749
            raise NotFoundError(name)
1670 by Canonical.com Patch Queue Manager
Big lot of database clean-up r=stub except for resolution of conflicts.
750
        else:
751
            return item
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
752
753
    def __iter__(self):
1716.3.31 by kiko
Order the bugtracker listing, by title, so the end-user is not confused
754
        for row in self.table.select(orderBy="title"):
1102 by Canonical.com Patch Queue Manager
Lucille had some XXXs which should have been NOTEs
755
            yield row
756
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
757
    def queryByBaseURL(self, baseurl):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
758
        """See `IBugTrackerSet`."""
6059.1.20 by Gavin Panella
Default title to baseurl when creating a bugtracker; Fix inconsistencies when searching for bugtrackers.
759
        # All permutations we'll search for.
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
760
        permutations = base_url_permutations(baseurl)
6059.1.20 by Gavin Panella
Default title to baseurl when creating a bugtracker; Fix inconsistencies when searching for bugtrackers.
761
        # Construct the search. All the important parts in the next
762
        # expression are lazily evaluated. SQLObject queries do not
763
        # execute any SQL until results are pulled, so the first query
764
        # to return a match will be the last query executed.
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
765
        matching_bugtrackers = chain(
766
            # Search for any permutation in BugTracker.
767
            BugTracker.select(
768
                OR(*(BugTracker.q.baseurl == url
769
                     for url in permutations))),
770
            # Search for any permutation in BugTrackerAlias.
771
            (alias.bugtracker for alias in
772
             BugTrackerAlias.select(
773
                    OR(*(BugTrackerAlias.q.base_url == url
6059.1.24 by Gavin Panella
Stop doing substring searches for bugtrackers.
774
                         for url in permutations)))))
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
775
        # Return the first match.
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
776
        for bugtracker in matching_bugtrackers:
5093.1.1 by Tom Berger
Fix bug #117452 by prefixing the base url of a new bug tracker with http:// if it isnt present
777
            return bugtracker
3691.186.1 by Bjorn Tillenius
make BugTrackerSet.queryByBaseURL ignore http vs. https differences.
778
        return None
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
779
7675.604.14 by Gavin Panella
Bring back the BugTrackerSet.search() method; it's needed for the web API.
780
    def search(self):
781
        """See `IBugTrackerSet`."""
782
        return BugTracker.select()
783
11562.3.2 by Robert Collins
Move Active/Inactive batch navigators to batching for reuse and define a more useful api for getting sets of trackers.
784
    def trackers(self, active=None):
785
        # Without context, cannot tell what store flavour is desirable.
786
        store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
787
        if active is not None:
13155.1.13 by Curtis Hovey
Use find().one() because the db has a sane constraint.
788
            clauses = [BugTracker.active == active]
11562.3.2 by Robert Collins
Move Active/Inactive batch navigators to batching for reuse and define a more useful api for getting sets of trackers.
789
        else:
790
            clauses = []
791
        results = store.find(BugTracker, *clauses)
792
        results.order_by(BugTracker.name)
793
        return results
794
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
795
    def ensureBugTracker(self, baseurl, owner, bugtrackertype,
796
        title=None, summary=None, contactdetails=None, name=None):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
797
        """See `IBugTrackerSet`."""
6059.1.20 by Gavin Panella
Default title to baseurl when creating a bugtracker; Fix inconsistencies when searching for bugtrackers.
798
        # Try to find an existing bug tracker that matches.
799
        bugtracker = self.queryByBaseURL(baseurl)
800
        if bugtracker is not None:
801
            return bugtracker
802
        # Create the bugtracker; we don't know about it.
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
803
        if name is None:
5613.1.21 by Graham Binns
Added collision avoidance and testing thereof.
804
            base_name = make_bugtracker_name(baseurl)
805
            # If we detect that this name exists already we mutate it
806
            # until it doesn't.
807
            name = base_name
808
            name_increment = 1
809
            while self.getByName(name) is not None:
810
                name = "%s-%d" % (base_name, name_increment)
811
                name_increment += 1
4631.1.12 by Tom Berger
don't allow to be null, as per mark's request
812
        if title is None:
6205.2.3 by Gavin Panella
Generate bug tracker titles with make_bugtracker_title.
813
            title = make_bugtracker_title(baseurl)
6059.1.20 by Gavin Panella
Default title to baseurl when creating a bugtracker; Fix inconsistencies when searching for bugtrackers.
814
        bugtracker = BugTracker(
815
            name=name, bugtrackertype=bugtrackertype,
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
816
            title=title, summary=summary, baseurl=baseurl,
817
            contactdetails=contactdetails, owner=owner)
818
        flush_database_updates()
819
        return bugtracker
820
2292 by Canonical.com Patch Queue Manager
[r=bjornt] make bugtrackers use auto forms
821
    @property
7675.604.1 by Gavin Panella
Rename BugTrackerSet.bugtracker_count to just count.
822
    def count(self):
823
        return IStore(self.table).find(self.table).count()
2048 by Canonical.com Patch Queue Manager
debbugssync, hct enabling, and ui fixes. r=jamesh
824
7675.604.3 by Gavin Panella
Get a list of all bug tracker names from a query instead of iterating through model objects.
825
    @property
826
    def names(self):
827
        return IStore(self.table).find(self.table).values(self.table.name)
828
2908.2.2 by Brad Bollenbach
fixes based on code review
829
    def getMostActiveBugTrackers(self, limit=None):
5308.1.20 by Gavin Panella
First lot of review changes suggested by Barry.
830
        """See `IBugTrackerSet`."""
7675.837.26 by Bryce Harrington
Review by gmb - requested clarification of table name in this routine
831
        store = IStore(BugTracker)
7675.837.21 by Bryce Harrington
Fix lintian issues
832
        result = store.find(
7675.837.26 by Bryce Harrington
Review by gmb - requested clarification of table name in this routine
833
            BugTracker,
834
            BugTracker.id == BugWatch.bugtrackerID)
835
        result = result.group_by(BugTracker)
7675.604.4 by Gavin Panella
Make getMostActiveBugTrackers() more efficient.
836
        result = result.order_by(Desc(Count(BugWatch)))
837
        if limit is not None:
2908.2.2 by Brad Bollenbach
fixes based on code review
838
            return result[:limit]
839
        else:
840
            return result
841
5742.1.4 by Christian Robottom Reis
Display product information for each bugtracker in the bugtracker index page. Add an API to IBugTrackerSet to allow us to fetch all products and projects in one fell swoop. Improve testing of IBugTrackerSet pages.
842
    def getPillarsForBugtrackers(self, bugtrackers):
843
        """See `IBugTrackerSet`."""
7675.110.3 by Curtis Hovey
Ran the migration script to move registry code to lp.registry.
844
        from lp.registry.model.product import Product
10724.1.1 by Henning Eggers
First batch of Project -> ProjectGrpoup renamings.
845
        from lp.registry.model.projectgroup import ProjectGroup
5742.1.4 by Christian Robottom Reis
Display product information for each bugtracker in the bugtracker index page. Add an API to IBugTrackerSet to allow us to fetch all products and projects in one fell swoop. Improve testing of IBugTrackerSet pages.
846
        ids = [str(b.id) for b in bugtrackers]
847
        products = Product.select(
848
            "bugtracker in (%s)" % ",".join(ids), orderBy="name")
10724.1.1 by Henning Eggers
First batch of Project -> ProjectGrpoup renamings.
849
        projects = ProjectGroup.select(
5742.1.4 by Christian Robottom Reis
Display product information for each bugtracker in the bugtracker index page. Add an API to IBugTrackerSet to allow us to fetch all products and projects in one fell swoop. Improve testing of IBugTrackerSet pages.
850
            "bugtracker in (%s)" % ",".join(ids), orderBy="name")
851
        ret = {}
852
        for product in products:
853
            ret.setdefault(product.bugtracker, []).append(product)
854
        for project in projects:
855
            ret.setdefault(project.bugtracker, []).append(project)
856
        return ret
857
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
858
859
class BugTrackerAlias(SQLBase):
860
    """See `IBugTrackerAlias`."""
861
    implements(IBugTrackerAlias)
862
863
    bugtracker = ForeignKey(
864
        foreignKey="BugTracker", dbName="bugtracker", notNull=True)
865
    base_url = StringCol(notNull=True)
866
867
868
class BugTrackerAliasSet:
869
    """See `IBugTrackerAliasSet`."""
870
    implements(IBugTrackerAliasSet)
871
5308.1.3 by Gavin Panella
Change aliases to be like IBug.tags.
872
    table = BugTrackerAlias
873
5308.1.12 by Gavin Panella
Working, but needs more comprehensive testing.
874
    def queryByBugTracker(self, bugtracker):
5308.1.1 by Gavin Panella
Interfaces and database classes for bug tracker aliases.
875
        """See IBugTrackerSet."""
5308.1.22 by Gavin Panella
Small changes as suggested by Barry in review.
876
        return self.table.selectBy(bugtracker=bugtracker.id)